junit测试参考官方文档:
https://docs.spring.io/spring-boot/docs/2.0.4.RELEASE/reference/html/boot-features-testing.html
1. 对springboot框架的项目进行测试,需要引入测试包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
然后在Test类上,创建springboot的引入
@SpringBootTest @RunWith(SpringRunner.class)
在新版本中用junit4:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes=MySpringApplication.class)
如果是web项目,对controller进行测试,则要引入webServletContext,不然无法获取request和response
@WebAppConfiguration
因Test类是独立运行,要对多个类进行测试,可能会建立多个类的测试类,这时就可以将统一类注解放在一个共通类。让使用类继承即可。(注意不能用接口)
常用测试配置:
-
@Ignore (忽略某项测试)
-
@FixMethodOrder(MethodSorters.NAME_ASCENDING) 注意:要定义在当前类上,不能定义到启动父类上,在junit5版本用TestMethodOrder代替
-
@TestConfiguration (仅用于测试的configuration)
-
@MockBean(用于容器启动,将某个类作为随机结果返回)
- Mockito
- @Mock & @InjectMocks (对于非容器启动类的测试)
- @Spy
- @ActiveProfiles (用于定义要使用的application文件)
-
@WebMvcTest(****Controller.class) (用于单纯测试Controller类)
-
@MybatisTest (mapper接口测试)
-
Suit(集合测试,用于将多个test用例合为一体测试)
- @Rule
-
TestCase 和 TestSuit (注解的原身)
-
@Parameterized.Parameters
@TestConfiguration
当我们需要引入配置来测试某个类时,就可以创建一个测试用的TestConfiguration,当然为了不和真正的使用有关联,我一般建立在test下,这样就不会有瓜葛。
为了测试MyHttpRequestMvcInterceptor,创建一个测试配置类:
@TestConfiguration public class MvcInterceptorAssistance extends WebMvcConfigurationSupport { /** * 在WebMvc配置中设立RequestCounter * @param registry */ @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new MyHttpRequestMvcInterceptor()); super.addInterceptors(registry); } }
假设这是我的TestController:
@RestController public class TestController { @GetMapping("/test") public String test(){ return "testResultOk"; } }
配置结构:
@RunWith(SpringJUnit4ClassRunner.class)
@Import(MvcInterceptorAssistance.class)
@AutoConfigureMockMvc
@SpringBootTest(classes = SpringMainTest.class,webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,properties = {"grpc.port=11809"}) public class SpringBootBaseTest {
测试Test1-testRestTemplate:
@Autowired private TestRestTemplate testRestTemplate; @LocalServerPort private int port; @Test public void test2() throws Exception { testRestTemplate.getForEntity("http://localhost:"+port + "/test",String.class); }
测试Test2 - MockMvc:
@Autowired private MockMvc mvc; @Test public void test() throws Exception { mvc.perform(MockMvcRequestBuilders.get("/test").accept(MediaType.APPLICATION_JSON)) .andExpect(MockMvcResultMatchers.status().isOk());
※ 对于Post的json格式,需要注意填写accept,还需要填写contentType,否则会出现类型不匹配。
@MockBean:
mockbean用于在@springboottest中将指定的类,不作为IOC托管中的bean来使用。即比如我们要测试@Controller的类,controller调用了多个service,其中一个service可能牵扯调用外部或者DB,那么我们就可以在测试中
将此类指定为@MockBean.
@RunWith(SpringRunner.class)
@SpringBootTest
public class MyControllerTest{
@MockBean
private TestService testService;
@Autowired
private MyController myController;
@Test
public void getUser(){
Mockito.when(testService.remoteGet(Mockito.any(User.class))).thenReturn(user);
User user = myController.getUser(1234);
}
※ @MockBean 用于IOC bean模式,这样@MockBean标记的对象,会抢占原来的bean在IOC中的位置。
@Mock & @InjectMocks
当我们有一个spring项目,但是我们又不想通过容器启动(@SpringBootTest),这时候就可以使用单独对bean测试的方式。
测试类使用@InjectMocks , 因测试的类内部会有@Autowired关联的类,关联类要使用@Mock。
@Mock的对象默认是不具备任何方法能力的。也就是类似于空对象,需要在test使用时来指定。
@RunWith(SpringRunner.class) @ActiveProfiles("test") public class AccountServiceTest { @InjectMocks private AccountServiceImpl accountService; @Mock private AccountDao accountDao; /** * service使用Mock模式调用 */ @Test public void testService(){ AccountDto accountDto = new AccountDto(); accountDto.setId("1234"); accountDto.setName("test1234"); Mockito.when(userDao.insert(Mockito.any(Account.class))).thenReturn(1); accountService.insert(accountDto); }
※ @InjectMocks 要使用具体实现类,不能使用接口定义。可用于spring不启动时的测试。
@Spy
spy的作用就是可以将我们new的对象传入到@MockInject的属性变量中。使用在代替@Bean创建的对象
例如:UserService内部会使用@Autowired RestTemplate restTemplate;这个时候就可以使用@Spy RestTemplate restTemplate = new RestTemplate();来代替。
Mockito:
VOID方法:
Mockito.doNothing().when(userService).addUser(Mockito.any(UserDto.class));
有返回值测试:
Mockito.when(userDao.insert(Mockito.any(Account.class))).thenReturn(1);
Mock的内容还可判断当前Inject传入的内容是否满足预期
Mockito.doNothing().when(myRedis).hset(Mockito.eq("USER"), Mockito.eq("admin123"), Mockito.eq(JSONObject.toJSONString(userInfo))).thenReturn(true);
因没有otherwise和反向匹配,当对预期值判断时可使用(双组合):
Mockito.when(myRedis.hdel(Mockito.anyString(), Mockito.anyString())).thenThrow(ValueMatcherException.class);
Mockito.when(myRedis.hdel(Mockito.eq("USER"), Mockito.eq("admin123"))).thenReturn(1L);
或者实现Answer接口,自己处理应该的返回值。
class MyAnswer implements Answer<java.lang.Long> { private UserDto userDto; public MyAnswer(UserDto userDto){ this.userDto = userDto; } @Override public java.lang.Long answer(InvocationOnMock invocation) throws Throwable { if (StringUtils.equals(invocation.getArgument(0), "User") && StringUtils.equals(invocation.getArgument(1), userDto.getUserCode())){ return 1L; } throw new IllegalArgumentException(); } }
Mockito.verify 仅测试某个mockbean是否有被执行,执行的次数,并不能判断内部的值。
@ActiveProfiles (用于定义要使用的application文件)
当我们属性文件定义 application-test.properties 内部定义了测试要使用的配置信息,那么我们就可以使用@ActiveProfiles("test")类似于 spring.active.profiles=test.
@WebMvcTest(****Controller.class)
用于单纯测试Controller类,通过@MockMvc 测试,其它的用@MockBean标记。
※注意:其实WebMvcTest还是会去查找应用中标注@SpringBootApplication的类,也就是如果应用的启动类中标记了ScanMapper,依然会加载mapperbean的相关对象,尽管当前的测试类跟
这个没有半毛钱关系。因此,需要在测试类内部新创建一个@SpringBootApplication的类,在这个类中取消mapperScan,这样就可以了。
@RunWith(SpringRunner.class) //使用@WebMvcTest只实例化Web层,而不是整个上下文。在具有多个Controller的应用程序中, @WebMvcTest(TestController.class) public class TestControllerTest {
@SpringBootApplication
@TestConfiguration
public static classTestControllerTest Config{
}
@Autowired private MockMvc mockMvc; @MockBean private MyService myService; @Test public void testUser() throws Exception { Mockito.when(myService.getUser("123")).thenReturn(null); this.mockMvc.perform(post("/user")) .contentType("application/json")) .andDo(print()) .andExpect(status().isOk()) .andExpect(null)); } }
但是如果遇到统一配置 WebMvcConfigurer 拦截器内部依赖的bean,则会出现bean实例错误。需要让ctroller绕开。
@RunWith(SpringRunner.class) @WebMvcTest(MyController.class) public class MyControllerTest { @Autowired private MyController myController; private MockMvc mockMvc; /** 通过覆盖 **/ @Before public void setup() { mockMvc = MockMvcBuilders.standaloneSetup(myController).build(); } /** * 拦截器内部的bean */ @MockBean private MyTestBean myTestBean; // ... }
@MybatisTest:
当我们想要测试Mapper层。就可以引入mybatis的test。
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter-test</artifactId> <version>1.3.2</version> <scope>test</scope> </dependency>
@RunWith(SpringRunner.class) @MybatisTest //添加以下设置,使用真实数据库 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 默认会回滚,可以通过以下设置调整不回滚 @Transactional(propagation = Propagation.NOT_SUPPORTED) public class UserMapperTest { @Autowired private UserMapper userMapper;
Suit集合测试:
@RunWith(Suite.class) @Suite.SuiteClasses({AAATest.class,BBBTest.class}) public class suitTest{}
因集合测试不同于单个测试会根据加载的class顺序执行测试用例,且不会中途退出应用启动,因此要注意测试用例之间不能对某个公有变量值有操作。
不同的测试内容分属不同的集合,较好。
TestCase 和 TestSuit (注解的原身)
我们使用中的@Test,其实也可以是继承TestCase类,方法public,void 以test开头。
public class Test1 extends TestCase { public void testApplication() { **** } }
@Suit用的是继承TestSuit类。内部方法名为suit,static,public返回Test。
public class SuitTest2 extends TestSuite { public static Test suit(){ TestSuite suite = new TestSuite(); // suite.addTestSuite(TestSuit3.class); //可添加子suit Test testApplication = TestSuite.createTest(Test2.class, "test1"); //添加Test suite.addTest(testApplication); return suite; } } /** * 测试Main类的方法 */ public static void main(String[] args) { TestResult result = new TestResult(); SuitTest2.suit().run(result); result.errorCount(); }
用于设定某项规则,然后处理test。这里rule规则只是给我们预留了对应的处理接口,具体的实现逻辑需要自行代码处理。
MethodRule:
方法执行Rule。
比如,某个Test要多次运行,参数又不一致,如果写到一个Test中,看着很凌乱,且代码量大大增加。这里我们可以自己写个类实现MethodRule接口,处理对应的逻辑代码。
这里写了个 ParamsMethodRule
/** * Description:参数定义Rule * * @author Denny.Zhao * @since 2021/11/22 */ public class ParamsMethodRule implements MethodRule { private final String testName; private final String[] strArray; public String params; public ParamsMethodRule(String[] strArray, String testName) { this.strArray = strArray; this.testName = testName; } public String getParams() { return params; } @Override public Statement apply(Statement base, FrameworkMethod method, Object target) { return new Statement() { @Override public void evaluate() throws Throwable { if (method.getName().equals(testName)){ int i=0; do{ params = strArray[i]; base.evaluate(); }while (++i < strArray.length); }else{ base.evaluate(); } } }; } }
使用的地方引入Rule规则:
@Rule public ParamsMethodRule rule = new ParamsMethodRule(new String[]{ "httpclient.springclient.stats.pending", "httpclient.springclient.stats.available", "httpclient.springclient.stats.leased", "httpclient.springclient.stats.total", "httpclient.springclient.stats.max" }, "httpClientSpringClientTest");
在@Test方法中通过以下的方式可以获取参数;
String param = rule.getParams();
对于重复性试验,可使用springTest中的@Repeat,需要添加以下Rule。
@Rule public final SpringMethodRule springMethodRule = new SpringMethodRule();
-
@Parameterized.Parameters
用于参数化入参项,重复跑数据用。
1. 定义参数内容
@Parameterized.Parameters public static Collection getName(){ return Lists.list("DescribeUser",
2. 构造化类
@RunWith(Parameterized.class) @Slf4j public class MyTest { private String userName; public SkiffAuthMgrControllerTest(String userName){ this.userName= userName; }
这样即可重复跑一个case数据了。
※注意:
JMX关闭:
由于junit默认是关闭对jmx的访问的,因此如果项目中有@Autowired MBeanServer,会获取为空,如果要打开,则参见:
具体见官方文档:https://docs.spring.io/spring-boot/docs/2.0.4.RELEASE/reference/html/boot-features-testing.html 43.3.6 Using JMX部分
@RunWith(SpringRunner.class) @SpringBootTest(properties = "spring.jmx.enabled=true") @DirtiesContext public class SampleJmxTests { @Autowired private MBeanServer mBeanServer; @Test public void exampleTest() { // ... } }