欢迎光临
我们一直在努力

Spring框架(3) 整合JUnit测试全攻略

一.Spring 整合 Junit 测试框架

基本概念

Spring 整合 Junit 是为了方便在 Spring 环境下进行单元测试和集成测试。通过 Spring 提供的测试支持,可以轻松地加载 Spring 容器、注入依赖以及进行事务管理等操作。

核心注解

@RunWith(SpringRunner.class)

  • 替代了传统的 Junit 运行器
  • 提供了 Spring 测试功能支持
  • 示例:

@RunWith(SpringRunner.class)
public class UserServiceTest {
// 测试代码
}

@ContextConfiguration

  • 用于指定 Spring 配置文件或配置类的位置
  • 两种使用方式:
  • 基于 XML 配置:
  • @ContextConfiguration(locations = "classpath:applicationContext.xml")

  • 基于 Java 配置:
  • @ContextConfiguration(classes = AppConfig.class)

依赖注入

@Autowired

  • 自动注入测试所需的 Spring Bean
  • 可以注入服务层、DAO 层等组件
  • 示例:

@Autowired
private UserService userService;

事务管理

@Transactional

  • 测试方法默认会在事务中执行
  • 测试完成后会自动回滚事务
  • 避免测试数据污染数据库
  • 示例:

@Test
@Transactional
public void testSaveUser() {
// 测试保存用户
}

高级特性

@TestPropertySource

  • 用于指定测试专用的属性文件
  • 覆盖主配置文件的属性
  • 示例:

@TestPropertySource(locations = "classpath:test.properties")

@MockBean

  • 用于在测试环境中模拟 Bean
  • 可以替换实际 Bean 或添加新的模拟 Bean
  • 示例:

@MockBean
private UserRepository userRepository;

测试示例

完整测试类示例:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = TestConfig.class)
@Transactional
public class UserServiceIntegrationTest {

@Autowired
private UserService userService;

@MockBean
private EmailService emailService;

@Test
public void testRegisterUser() {
User user = new User("test", "test@example.com");
when(emailService.sendWelcomeEmail(anyString())).thenReturn(true);

User registered = userService.register(user);

assertNotNull(registered);
assertEquals("test", registered.getUsername());
verify(emailService).sendWelcomeEmail(user.getEmail());
}
}

最佳实践

  • 保持测试独立性和可重复性
  • 合理使用模拟对象(Mock)减少外部依赖
  • 为不同的测试场景创建专门的测试配置类
  • 使用内存数据库(H2, HSQLDB)进行集成测试
  • 区分单元测试和集成测试目录结构
  • 二、Spring整合JDBC实现转账业务

  • 数据准备

    • 创建银行账户表account,包含字段:

      CREATE TABLE account (
      id INT PRIMARY KEY AUTO_INCREMENT,
      name VARCHAR(50) NOT NULL,
      balance DECIMAL(10,2) NOT NULL
      );

    • 插入测试数据:

      INSERT INTO account(name, balance) VALUES('张三', 1000),('李四', 500);

  • 配置数据源 在Spring配置文件中配置DataSource:

  • <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/bank"/>
    <property name="username" value="root"/>
    <property name="password" value="123456"/>
    </bean>

  • 创建JdbcTemplate

  • <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
    <property name="dataSource" ref="dataSource"/>
    </bean>

  • 实现转账业务

    • 创建AccountDao接口:

    public interface AccountDao {
    void out(String outUser, double money);
    void in(String inUser, double money);
    }

    • 实现类使用JdbcTemplate:

    @Repository
    public class AccountDaoImpl implements AccountDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void out(String outUser, double money) {
    jdbcTemplate.update("UPDATE account SET balance=balance-? WHERE name=?", money, outUser);
    }

    @Override
    public void in(String inUser, double money) {
    jdbcTemplate.update("UPDATE account SET balance=balance+? WHERE name=?", money, inUser);
    }
    }

  • 添加事务管理

    • 配置事务管理器:

    <bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
    </bean>

    • 开启注解事务支持:

    <tx:annotation-driven transaction-manager="transactionManager"/>

    • 在Service层添加事务注解:

    @Service
    public class AccountService {

    @Autowired
    private AccountDao accountDao;

    @Transactional
    public void transfer(String outUser, String inUser, double money) {
    accountDao.out(outUser, money);
    // 模拟异常
    // int i = 1/0;
    accountDao.in(inUser, money);
    }
    }

  • 测试转账功能

  • @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration("classpath:applicationContext.xml")
    public class AccountTest {

    @Autowired
    private AccountService accountService;

    @Test
    public void testTransfer() {
    accountService.transfer("张三", "李四", 100);
    }
    }

  • 注意事项

    • 确保数据库连接参数正确
    • 事务传播行为默认是REQUIRED
    • 异常回滚默认只针对RuntimeException
    • 可通过@Transactional的属性自定义事务行为

    三、动态代理

    动态代理是一种在运行时动态创建代理类和代理对象的技术,它允许我们在不修改原始类代码的情况下,为对象添加额外的功能或控制访问。与静态代理相比,动态代理更加灵活和高效,适用于需要为多个类或接口创建代理的场景。

    1. 动态代理的特点

    • 运行时生成:代理类是在程序运行时动态生成的,不需要预先编写代理类代码
    • 通用性强:一个动态代理类可以代理多个不同的接口或类
    • 减少代码量:避免了为每个被代理类编写单独的代理类
    • 灵活性高:可以在运行时决定代理哪些方法以及如何代理

    2. Java中的实现方式

    在Java中,主要有两种实现动态代理的方式:

    (1) JDK动态代理

    基于接口的动态代理,要求被代理类必须实现至少一个接口。

    实现步骤:

  • 定义业务接口
  • 实现业务接口(被代理类)
  • 实现InvocationHandler接口
  • 通过Proxy.newProxyInstance()创建代理对象
  • 示例代码:

    public interface UserService {
    void save();
    }

    public class UserServiceImpl implements UserService {
    public void save() {
    System.out.println("保存用户");
    }
    }

    public class LogHandler implements InvocationHandler {
    private Object target;

    public LogHandler(Object target) {
    this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println("开始记录日志…");
    Object result = method.invoke(target, args);
    System.out.println("日志记录完成");
    return result;
    }
    }

    // 使用方式
    UserService userService = new UserServiceImpl();
    UserService proxy = (UserService) Proxy.newProxyInstance(
    userService.getClass().getClassLoader(),
    userService.getClass().getInterfaces(),
    new LogHandler(userService)
    );
    proxy.save();

    (2) CGLIB动态代理

    基于继承的动态代理,可以对没有实现接口的类进行代理。

    实现步骤:

  • 创建被代理类
  • 实现MethodInterceptor接口
  • 通过Enhancer创建代理对象
  • 示例代码:

    public class UserDao {
    public void save() {
    System.out.println("保存用户数据");
    }
    }

    public class LogInterceptor implements MethodInterceptor {
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    System.out.println("开始记录日志…");
    Object result = proxy.invokeSuper(obj, args);
    System.out.println("日志记录完成");
    return result;
    }
    }

    // 使用方式
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(UserDao.class);
    enhancer.setCallback(new LogInterceptor());
    UserDao proxy = (UserDao) enhancer.create();
    proxy.save();

    3. 动态代理的应用场景

    • AOP编程:实现日志记录、性能监控、事务管理等横切关注点
    • RPC框架:远程方法调用的客户端代理
    • Mock测试:为测试创建代理对象
    • 权限控制:方法调用前的权限校验
    • 缓存代理:为方法调用添加缓存功能

    4. JDK代理与CGLIB代理比较

    特性JDK动态代理CGLIB动态代理
    代理方式 基于接口 基于继承
    目标类要求 必须实现接口 可以代理普通类
    性能 创建速度较快,调用稍慢 创建速度较慢,调用较快
    方法拦截 只能拦截接口方法 可以拦截所有非final方法
    依赖 JDK自带 需要额外引入CGLIB库

    5. 注意事项

    • 使用JDK动态代理时,被代理类必须实现接口
    • CGLIB不能代理final类或final方法
    • 动态代理会增加一定的性能开销
    • 复杂的代理逻辑可能会影响程序的可读性
    • 在Spring框架中,默认使用JDK动态代理,如果没有接口则使用CGLIB

    四、Spring AOP机制详解

    4.1 AOP基本概念

    AOP(Aspect-Oriented Programming)即面向切面编程,是Spring框架的核心模块之一。它通过预编译方式和运行期动态代理实现程序功能的统一维护。AOP主要解决的是横切关注点问题,将与业务无关但需要大量重复的代码(如日志记录、事务管理、安全检查等)从业务逻辑中分离出来。

    4.1.1 AOP术语解析

  • 切面(Aspect):横切关注点的模块化,包含通知和切点。例如日志记录模块可以定义为一个切面。
  • 连接点(Joinpoint):程序执行过程中的特定点,如方法调用或异常抛出。Spring AOP中仅支持方法级别的连接点。
  • 通知(Advice):在特定连接点上执行的动作,包括:
    • 前置通知(Before advice)
    • 后置通知(After advice)
    • 返回通知(After-returning advice)
    • 异常通知(After-throwing advice)
    • 环绕通知(Around advice)
  • 切点(Pointcut):匹配连接点的谓词,用于确定哪些连接点会应用通知。
  • 引入(Introduction):为现有类添加新方法或属性。
  • 目标对象(Target object):被一个或多个切面通知的对象。
  • AOP代理(AOP proxy):由AOP框架创建的对象,用于实现切面契约。
  • 4.2 Spring AOP实现机制

    4.2.1 代理模式

    Spring AOP默认使用JDK动态代理和CGLIB代理两种方式:

  • JDK动态代理:基于接口实现,要求目标类必须实现至少一个接口。运行时动态生成代理类,通过反射机制调用目标方法。

    public interface UserService {
    void saveUser();
    }

    public class UserServiceImpl implements UserService {
    public void saveUser() {
    System.out.println("保存用户");
    }
    }

  • CGLIB代理:基于继承实现,不需要接口。通过生成目标类的子类来拦截方法调用。对于final类或方法无法使用CGLIB代理。

  • 4.2.2 织入(Weaving)时机

    Spring AOP采用运行时织入方式,即在应用运行期间通过代理机制将切面逻辑织入到目标对象中。这与AspectJ等框架提供的编译时织入和类加载时织入不同。

    4.3 Spring AOP配置方式

    4.3.1 基于XML的配置

    <aop:config>
    <aop:aspect id="logAspect" ref="loggingAspect">
    <aop:pointcut id="servicePointcut"
    expression="execution(* com.example.service.*.*(..))"/>
    <aop:before method="beforeAdvice" pointcut-ref="servicePointcut"/>
    </aop:aspect>
    </aop:config>

    4.3.2 基于注解的配置

    @Aspect
    @Component
    public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void beforeAdvice(JoinPoint joinPoint) {
    System.out.println("方法执行前:" + joinPoint.getSignature().getName());
    }

    @AfterReturning(pointcut="execution(* com.example.service.*.*(..))",
    returning="result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
    System.out.println("方法返回后:" + result);
    }
    }

    4.4 切点表达式语法

    Spring AOP使用AspectJ切点表达式语言定义切点:

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

    常见表达式示例:

  • execution(* com.example.service.*.*(..)) – 匹配service包下所有类的所有方法
  • execution(public * *(..)) – 匹配所有public方法
  • execution(* set*(..)) – 匹配所有set开头的方法
  • within(com.example.service..*) – 匹配service包及其子包下的所有方法
  • @annotation(com.example.Loggable) – 匹配带有@Loggable注解的方法
  • 4.5 AOP应用场景

  • 日志记录:统一记录方法调用日志
  • 事务管理:声明式事务处理
  • 权限控制:方法调用前的权限校验
  • 性能监控:方法执行时间统计
  • 异常处理:统一异常捕获和处理
  • 缓存处理:方法结果缓存
  • 4.6 AOP最佳实践

  • 尽量使用注解方式配置AOP,代码更简洁
  • 切点表达式应尽可能精确,避免影响过多方法
  • 通知方法应保持简洁,避免复杂业务逻辑
  • 注意代理对象的方法调用问题,避免自调用导致AOP失效
  • 对于性能敏感场景,考虑使用编译时织入的AspectJ替代Spring AOP
  • 4.7 AOP与IOC的关系

    Spring AOP与IOC容器紧密集成:

    • AOP代理对象由IOC容器管理
    • 切面组件可以像普通Bean一样被依赖注入
    • AOP配置可以与其他Bean配置统一管理

    这种集成使得AOP在Spring应用中能够无缝工作,开发者无需关心底层代理机制的实现细节。

    五、Spring基于注解的AOP配置

  • 常用注解说明

    • @Aspect:标注当前类为切面类
    • @Before:前置通知,在目标方法执行前执行
    • @AfterReturning:后置通知,在目标方法正常执行后执行
    • @AfterThrowing:异常通知,在目标方法抛出异常后执行
    • @After:最终通知,无论目标方法是否正常执行都会执行
    • @Around:环绕通知,可以控制目标方法的执行
    • @Pointcut:定义切入点表达式
  • 配置步骤详解 (1) 在Spring配置文件中启用AOP注解支持:
  • <aop:aspectj-autoproxy/>

    (2) 定义切面类并添加@Component注解:

    @Component
    @Aspect
    public class LogAspect {
    // 切入点表达式示例
    @Pointcut("execution(* com.example.service.*.*(..))")
    private void serviceLayer(){}

    @Before("serviceLayer()")
    public void beforeAdvice(JoinPoint joinPoint){
    System.out.println("前置通知:方法"+joinPoint.getSignature().getName()+"即将执行");
    }
    }

  • 切入点表达式语法

    • execution:匹配方法执行
    • within:匹配类型
    • args:匹配参数
    • target:匹配目标对象
    • this:匹配代理对象
    • bean:匹配Bean名称
  • 实际应用场景 (1) 日志记录:在方法执行前后记录日志 (2) 性能监控:统计方法执行时间 (3) 事务管理:通过环绕通知实现事务控制 (4) 权限校验:在方法执行前进行权限验证 (5) 缓存处理:方法执行结果缓存

  • 注意事项

    • 确保切面类被Spring容器管理(添加@Component等注解)
    • 环绕通知需要显式调用proceed()方法
    • 多个切面的执行顺序可通过@Order注解控制
    • 注意切入点表达式的性能影响,避免过于宽泛的匹配
    赞(0)
    未经允许不得转载:171主机测评 » Spring框架(3) 整合JUnit测试全攻略
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址