SpringBoot自动进行单元测试

pom.xml

加入依赖和配置文件

<dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-library</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <scope>test</scope>
        </dependency>
        ...
        <resource>
                <filtering>true</filtering>
                <directory>${basedir}/src/test/resources</directory>
                <includes>
                    <include>**/*.yaml</include>
                </includes>
            </resource>

bootstrap.yaml

相对于普通发布可以剔除一些不用的配置(例如MQ,我放到了service)

spring:
  main:
    allow-bean-definition-overriding: true
  profiles:
    active: ${ENVIRONMENT:test}
  application:
    name: unit-test
  cloud:
    nacos:
      config:
        file-extension: yaml
  liquibase:
    change-log: classpath:/db/changelog/liquibase/query-web.xml
logging:
  level:
    root: info
    io.lettuce.core.protocol: ERROR
  tracing:
    web:
management:
  endpoint:
    health:
      probes:
        enabled: true
  endpoints:
    web:
      exposure:
        include: health,prometheus,info

logback.output.file.dir: ./logs

application-test.yaml

spring:
  redis:
    lettuce:
      pool:
        max-idle: 4
        min-idle: 1
    host: localhost
    database: 10
  shardingsphere:
    props:
      sql-show: true
    datasource:
      enabled: true
      names: master,slave
      common:
        type: com.alibaba.druid.pool.DruidDataSource
      master:
        driver-class-name: com.mysql.cj.jdbc.Driver
        type: com.alibaba.druid.pool.DruidDataSource
        password: database_password
        username: root
        url: >-
          jdbc:mysql://localhost:3306/${spring.liquibase.default-schema}?
          characterEncoding=utf8&
          zeroDateTimeBehavior=convertToNull&
          useSSL=false&
          useJDBCCompliantTimezoneShift=true&
          useLegacyDatetimeCode=false&
          serverTimezone=GMT%2B8&
          allowMultiQueries=true&
          allowPublicKeyRetrieval=true
      slave:
        driver-class-name: ${spring.shardingsphere.datasource.master.driver-class-name}
        type: ${spring.shardingsphere.datasource.master.type}
        password: ${spring.shardingsphere.datasource.master.password}
        username: ${spring.shardingsphere.datasource.master.username}
        url: ${spring.shardingsphere.datasource.master.url}
    rules:
      readwrite-splitting:
        data-sources:
          default:
            name: default
            write-data-source-name: master
            read-data-source-names: slave
            load-balancer-name: default
        load-balancers:
          default:
            type: ROUND_ROBIN
            props:
              parameter: default
  liquibase:
    default-schema: backend_for_frontend
    liquibase-schema: ${spring.liquibase.default-schema}
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

TestApplication.java

创建用于测试的SpringBoot启动类


import com.google.common.eventbus.EventBus;
import org.apache.ibatis.annotations.Mapper;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;

/**
 * @author Qbit
 * @date 2022-06-15
 */
@EnableTransactionManagement()
@MapperScan(value = "com.qbit.*Mapper",annotationClass = Mapper.class)
@SpringBootApplication
@Configuration
@EnableJpaRepositories
public class TestApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestApplication.class, args);
    }

    @Bean
    public EventBus eventBus(){
        return new EventBus();
    }
}

TestUtils.java

通过反射自动测试的工具


import com.google.common.base.Preconditions;
import com.qbit.QueryUtils;
import com.qbit.code.CodeUtils;
import com.qbit.pagination.PaginationParameter;
import com.qbit.web.Link;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiParam;
import lombok.var;
import org.hamcrest.MatcherAssert;
import org.hamcrest.core.IsEqual;
import org.hamcrest.core.IsNull;
import org.junit.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.stream.Stream;

public class TestUtils {
    public static void valueObject(Class<?> v, Object vo) {
        Preconditions.checkNotNull(v,"the ViewObject class must not be null:"+(null==vo?"":vo.getClass().getName()));
        Preconditions.checkArgument(v.getSimpleName().endsWith("ViewObject"),v.getName()+" is not a ViewObject class");
        Assert.assertNotNull(v.getName(),vo);
        Preconditions.checkArgument(v.isAssignableFrom(vo.getClass()),vo.getClass()+" is not the child of "+v);
        if(v.isInterface()) {
            Stream.of(v.getMethods())
                    .forEach(method -> {
                        try {
                            method(method, vo);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    });
        }else{
            //todo 暂时不处理继承的情况
            Stream.of(v.getDeclaredFields())
                    .forEach(field -> {
                        try {
                            field(field,vo);
                        }catch (Exception e){
                            throw new RuntimeException(e);
                        }
                    });
        }
    }

    private static void field(Field field, Object result) throws IllegalAccessException {
        ReflectionUtils.makeAccessible(field);
        ApiModelProperty property=field.getAnnotation(ApiModelProperty.class);
        String message = field.getDeclaringClass()+"."+field.getName();
        Object value=field.get(result);
        Class<?> clazz=field.getType();
        notEmpty(message,property,value,clazz);
    }

    private static void method(Method method, Object result) throws InvocationTargetException, IllegalAccessException {
        String message = method.getDeclaringClass()+"."+method.getName();
        Preconditions.checkArgument(method.getName().startsWith("get"), message +" is not a getter");
        Preconditions.checkArgument(method.getParameterCount()==0, message +" must not have parameter");
        ApiModelProperty property=method.getAnnotation(ApiModelProperty.class);
        Object value=method.invoke(result);
        Class<?> clazz=method.getReturnType();
        notEmpty(message, property, value, clazz);
    }

    private static void notEmpty(String message, ApiModelProperty property, Object value, Class<?> clazz) {
        Preconditions.checkNotNull(property, message +" should have annotation "+ApiModelProperty.class.getSimpleName());
        MatcherAssert.assertThat(message, value, IsNull.notNullValue());
        var example= property.example();
        if(!StringUtils.isEmpty(example)){
            if(LocalDateTime.class== clazz){
                //todo
            }else{
                MatcherAssert.assertThat(message, value.toString(), IsEqual.equalTo(property.example()));
            }
        }else{
            if(clazz ==String.class|| clazz ==Integer.class|| clazz == BigDecimal.class|| clazz ==LocalDateTime.class){
                CodeUtils.doNothing();
            }else if(clazz==Link.class){
                CodeUtils.doNothing();
            }else if(Collection.class.isAssignableFrom(clazz)){
                Collection<?> collection=(Collection<?>) value;
                Assert.assertTrue(message+collection.size(),collection.size()>1);
                for(Object item:collection){
                    Assert.assertNotNull(message+" element is null",item);
                    //todo
                }
            }else {
                valueObject(clazz, value);
            }
        }
    }

    public static void controller(Class<?> api,Object impl) {
        Stream.of(api.getMethods()).parallel()
                .forEach(method -> {
                    example(impl, method);
                });
    }

    private static void example(Object controller, Method method) {
        var parameters=new Object[method.getParameterCount()];
        for(int i=0;i<parameters.length;i++){
            var parameter= method.getParameters()[i];
            String message="the "+i+"th parameter of "+(method.getDeclaringClass().getName()+'.'+ method.getName());
            var apiPara=parameter.getAnnotation(ApiParam.class);
            Class<?> type = parameter.getType();
            if(PaginationParameter.class== type){
                parameters[i]= QueryUtils.firstPage();
                continue;
            }
            if(null==apiPara){
                Preconditions.checkArgument(type!=String.class,message);
                parameters[i]= example(type);
                continue;
            }
            String example=apiPara.example();
            Preconditions.checkArgument(!StringUtils.isEmpty(example),message);
            Preconditions.checkArgument(String.class==parameter.getType(),message);
            parameters[i]=example;
        }
        Object result=null;
        try{
            result= method.invoke(controller,parameters);
        }catch (Exception e){
            throw new RuntimeException(e);
        }
//                    valueObject(method.getReturnType(),result);
    }

    private static Object example(Class<?> type) {

        try{
            var out=type.newInstance();
            Stream.of(type.getDeclaredFields()).forEach(field -> {
                String message=type.getName()+"."+field.getName();
                ReflectionUtils.makeAccessible(field);
                var property=field.getAnnotation(ApiModelProperty.class);
                Preconditions.checkNotNull(property,message);
                var example=property.example();
//                Preconditions.checkArgument(!StringUtils.isEmpty(example),message);
                if(StringUtils.isEmpty(example)){
                    return;
                }
                var fieldType=field.getType();
                Object value=null;
                if(fieldType==String.class){
                    value=example;
                }else if(fieldType==Integer.class){
                    value=Integer.valueOf(example);
                }else{
                    // todo
                }
                try {
                    field.set(out, value);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }
            });
            return out;
        }catch (Exception e){
            throw new RuntimeException(e);
        }

    }
}

XxxTest

标准的单元测试,测试一个方法


import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;

@RunWith(SpringRunner.class)
@WebAppConfiguration
//@TransactionConfiguration(transactionManager="transactionManager",defaultRollback=true)

public class SearchTests {
    @Test
    void search(){

    }
}

自动检查出参是否与example一致

@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = TestApplication.class)
public class SomeTests {
    @Autowired
    private SomeController controller;
    @Test
    public void get(){
        var vo=controller.get(ID);
        TestUtils.valueObject(SomeController ViewObject.class,vo);
    }
}

自动检查一个controller里所有接口

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;

@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = TestApplication.class)
public class ResumeTests {
    @Autowired
    private ResumeController controller;
    @Test
    public void testAll(){
        TestUtils.controller(ResumeController.class,controller);
    }
}

获取所有controller并测试

import lombok.Setter;
import lombok.var;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.web.bind.annotation.RestController;

@RunWith(SpringRunner.class)
@WebAppConfiguration
@SpringBootTest(classes = TestApplication.class)
@Setter
public class ControllerTest implements ApplicationContextAware {
    private ApplicationContext applicationContext;
    @Test
    public void testAll(){
        var controllers=applicationContext.getBeansWithAnnotation(RestController.class);
        controllers.values().parallelStream().forEach(controller->{
            TestUtils.controller(controller.getClass().getInterfaces()[0],controller);
        });
    }

}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值