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);
});
}
}