测试用例需要根据具体功能进行编写,需要将关注的功能点都测试到。测试的基本策略是,输入一个满足某个业务场景的数据,看得到的输出是否是期望的值。当预期没有达到时,我们修改现有的代码来达到预期。不断重复这个过程,尽最大可能覆盖可以考虑到的功能点。从而为提交的代码提供最基本的验证。
最近的单元测试编写,一开始我采用的是@SpringBootTest。但是由于我测试的类所在的package下面有很多其他的类,这些类的依赖比较复杂,当我采用@Autowired注入我测试的类时,需要去处理很多与该类无关的依赖信息。然后,我做了一个折衷:被测试的类采用new构建,它的依赖采用@MockBean注入。为了将这些采用@MockBean的依赖加载到我的测试类里,我将测试类里面的对应的依赖采用构造注入,我修改了被测试类的源码,如下:
class MyService {
//@Autowired //最初我用的是@Autowired, 因为我觉得这样比较简洁
private Class1 class1;
//@Autowired
private Class2 class2;
public MyService(Class1 class1, Class2 class2){
this.class1 = class1;
this.class2 = class2;
}
}
单元测试用例里就用这个构造器来初始化这个类,如下:
@RunWith(SpringRunner.class)
@SpringBootTest
class MyServiceTest {
@MockBean
private Class1 class1;
@MockBean
private Class2 class2;
private MyService service;
@Before
public void setUp(){
service = new MyService(class1, class2);
}
}
因为我的服务类里面有调用第三方的rest服务,所以我采用了WireMockServer来mock对rest服务的调用。测试用例通过以上的方式运行起来了,代码覆盖率还ok。但是,有一些功能点没有测试到。代码里面有的功能依赖了配置参数,而这些参数是通过@Value的方式注入的,要让这些配置生效,就必须采用Spring的方式注入服务类,而注入服务类的话,需要解决很多与当前功能无关的依赖,这不是我想处理的。而且目前采用SpringBootTest做单元测试,每次启动都会加载很多依赖,我觉得这是不必要的。
最后,我放弃了已经写好的单元测试,而采用纯Mockito的方式重写单元测试。我采用的方式是:每个类的单元测试只关注当前类本身,所有的依赖都采用@Mock的方式注入被测试类。而在之前的测试用例中,我写了很多对rest服务调用的假设(采用WireMock的方式,现在我觉得与rest服务的交互的测试,应该用rest服务的代理类来测试),我觉得是没有必要的,只需要假设一个期望的返回结果就可以了。因为对于当前的测试类,需要测试的只是该类包含的我们关注的业务或功能逻辑。明白了这一点,我很快重写好了单元测试。
@RunWith(MockitoJunitRunner.class)
class MyServiceTest {
@Mock
private Class1 class1;
@Mock
private Class2 class2;
@InjectMocks
private MyService service;
@Test
public void testFeature1_shouldxxxxWhenxxx(){...}
@Test
public void testFeature2_shouldxxxxWhenxxx(){...}
}
现在,因为无需加载Spring的依赖,用例的执行快了一些。
但是,还是之前的问题,采用@Value注入的参数为null,有一些功能点没测试到。如下:
@Value("${display.conent1}")
private String content1;
@Value("${display.content2}")
private String content2;
void setDisplayContent(Display display, Criteria criteria){
if(criteria.flagVal == 1)
display.setContent(content1);
else if(criteria.flagVal == 2)
display.setContent(content2);
}
最后,我采用反射的方式来注入这些参数(问题解决了),如下:
@Before
public void setUp(){
try{
Field content1 = service.getClass().getDeclaredField("content1");
content1.setAccessible(true);
content1.set(service, "xxxxx1");
Field content2 = service.getClass().getDeclaredField("content2");
content2.setAccessible(true);
content2.set(service, "xxxxx2");
} catch(Exception ex){//...}
}
我的总结是:
1. 单元测试只需要测试当前类负责的功能,依赖都采用mock的方式;
2. 用最少的依赖来完成单元测试的编写;
3. 每个类尽量做到职责单一。即便不是单一的功能,但至少从业务或功能的角度来看,它负责处理的必须是同一类工作,可能是几个过程一起来达到一个目的。通过将各个职责用单独的类封装起来之后,我们mock的时候会更加方便。将同一个或同一种职责封装在一个类里面,可以减少各个类之间不必要的依赖,因为该类必须的过程已经包含在它自己的上下文中,它的功能的各个部分已经自包含,从而它依赖或调用的必然是在它的职责之外的一些功能组件。因此,我们需要合理的划分功能,以让它们之间的交互最少,从而可以得到一个简洁稳定,易于维护的系统。当然,我们不会一开始就得到,但在不断的分析和修改之后,各个功能之间的界限会日渐清晰。