阿里妹导读:测试不该该是一门很高大尚的技术,应该是咱们技术人的基本功。但如今好像慢慢地,单元测试已经脱离了基本功的范畴。笔者曾经在不一样团队中推过单元测试,要求过覆盖率,但发现实施下去很难。后来在不停地刻意练习后,发现阻碍写UT的只是笔者的心魔,并非时间和项目的问题。在通过一些项目的实践后,也是有了一些本身的理解和实践,但愿和你们分享一下,和你们探讨下如何克服“单元测试”的心魔。css
文末福利:开发者成长计划,最强助力!html
红:测试先行,如今尚未任何实现,跑UT的时候确定不过,测试状态是红灯。编译失败也属于“红”的一种状况。
前端
绿:当咱们用最快,最简单的方式先实现,而后跑一遍UT,测试会经过,变成“绿”的状态。java
重构:看一下系统中有没有要重构的点,重构完,必定要保证测试是“绿”的。
web
@RunWith(SpringBootRunner.class)@DelegateTo(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = {Application.class})public class ApiServiceTest {
@Autowired ApiService apiService;
@Test public void testMobileRegister() { AlispResult<Map<String, Object>> result = apiService.mobileRegister(); System.out.println("result = " + result); Assert.assertNotNull(result); Assert.assertEquals(54,result.getAlispCode().longValue());
AlispResult<Map<String, Object>> result2 = apiService.mobileRegister(); System.out.println("result2 = " + result2); Assert.assertNotNull(result2); Assert.assertEquals(9,result2.getAlispCode().longValue());
AlispResult<Map<String, Object>> result3 = apiService.mobileRegister(); System.out.println("result3 = " + result3); Assert.assertNotNull(result3); Assert.assertEquals(200,result3.getAlispCode().longValue()); }
@Test public void should_return_mobile_is_not_correct_when_register_given_a_invalid_phone_number() { AlispResult<Map<String, Object>> result = apiService.mobileRegister(); Assert.assertNotNull(result); Assert.assertFalse(result.isSuccess()); }}
should:返回值,应该产生的结果
springwhen:哪一个方法sql
given:哪一个场景
typescript

契约测试:测试服务与服务之间的契约,接口保证。代价最高,测试速度最慢。
数据库
集成测试(Integration):集成当前spring容器、中间件等,对服务内的接口,或者其余依赖于环境的方法的测试。
json
// 加载spring环境@RunWith(SpringBootRunner.class)@DelegateTo(SpringJUnit4ClassRunner.class)@SpringBootTest(classes = {Application.class})public class ApiServiceTest {
@AutowiredApiService apiService;//do some test}
单元测试(Unit Test):纯函数,方法的测试,不依赖于spring容器,也不依赖于其余的环境。

一个类里面测试太多怎么办?
不知作别人mock了哪些数据怎么办?
测试结构太复杂?
测试莫名奇妙起不来?

经过组合Fixture(固定设施),来构造一个Scenario(场景)。
经过组合Scenario(场景)+ Fixture(固定设施),构造一个case(用例)。

Case:当用户正常登陆后,获取当前登陆信息时,应该返回正确的用户信息。这是一个简单的用户登陆的case,这个case里面总共有两个动做、场景,一个是用户正常登陆,一个是获取用户信息,演化为两个scenario。
Scenario:用户正常登陆,确定须要登陆参数,如:手机号、验证码等,另外隐含着数据库中应该有一个对应的用户,若是登陆时须要与第三方系统进行交互,还须要对第三方系统进行mock或者stub。获取用户信息时,确定须要上一阶段颁发的凭证信息,另外该凭证多是存储于一些缓存系统的,因此还须要对中间件进行mock或者stub。
Fixture
利用Builder模式构造请求参数。
利用DataFile来存储构造用户的信息,例如DB transaction进行数据的存储和隔离。
利用Mockito进行三方系统、中间件的Mock。
public class GetUserInfoCase extends BaseTest { private String accessToken;
@Autowired private UserFixture userFixture;
/** * 通用场景的mock */ @Before public void setUp() { //三方系统mock userFixture.whenFetchUserInfoThenReturn("1", new UserVO());
//依赖的其余场景 accessToken = new SimpleLoginScenario() .mobile("1234567890") .code("aaa") .login() .getAccessToken(); }
/** * BDD的三段式 */ @Test public void should_return_user_info_when_user_login_given_a_effective_access_token() { Response userInfoResponse = new GetUserInfoScenario() .accessToken(accessToken) .getUserInfo();
assertThat(userInfoResponse.jsonPath().getString("id"), equals("1")); }}
@Datapublic class SimpleLoginScenario { // 请求参数 private String mobile; private String code;
// 登陆结果 private String accessToken;
public SimpleLoginScenario mobile(String mobile) { this.mobile = mobile; return this; }
public SimpleLoginScenario code(String code) { this.code = code; return this; }
//登陆,而且保存AccessToken,这里返回自身,是由于有可能返回参数是多个。 public SimpleLoginScenario login() { Response response = loginWithResponse(); this.accessToken = response.jsonPath().getString("accessToken"); return this; }
//利用RestAssured进行登陆,这个方法能够是public,也能够经过参数传递一些验证方法 private Response loginWithResponse() { return RestAssured.get(API_PATH, ImmutableMap.of("mobile", mobile, "code", code)) .thenReturn(); }
}
Fixture
public class MockitoTest { @MockBean(classes = CacheImpl.class) private Cache cache;
@Test public void should_return_success() { // 固定参数,固定返回值 Mockito.when(cache.get("KEY")).thenReturn("VALUE");
// 动态参数,固定返回值 Mockito.when(cache.get(Mockito.anyString())).thenReturn("VALUE");
// 动态参数,固定返回值 Mockito.when(cache.get(Mockito.anyString())).then((invocation) -> { String key = (String) invocation.getArguments()[0]; return "VALUE"; });
// 固定参数,异常 Mockito.when(cache.get("KEY")).thenThrow(new RuntimeException("ERROR"));
// 验证调用次数 Mockito.verify(cache.get("KEY"), Mockito.times(1)); }}
(b)stub
//使用spring的@Primary来替换一个bean,若是不一样的测试须要的bean不一样,推荐使用@Configuration + @Import的方式,动态加载Bean@Primary@Component("cache")public class CacheStub implements Cache {
@Override public String get(String key) { return null; }
@Override public int setex(String key, Integer ttl, String element) { return 0; }
@Override public int incr(String key, Integer ttl) { return 0; }
@Override public int del(String key) { return 0; }}
使用@Transactional在一些测试的类上,这样在跑完测试后,数据不会commit,会回滚。但若是测试中对事物的传播有特殊要求,可能不适用。
通用的trancateAll和initSQL经过在每一个测试前跑清除数据、mock数据的脚本,来达到每一个测试对应一个隔离环境,这样数据间就不会产生干扰。
PowerMockito.mockStatic(C.class);PowerMockito.when(C.isTrue()).thenReturn(true);
注意:
PowerMock不只仅是用来mock静态方法的。
不建议mock静态方法,由于静态方法的使用场景都是些纯函数,大部分的纯函数不须要mock。部分静态方法依赖于一些环境和数据,针对这些方法,须要考虑下究竟是要mock其依赖的数据和方法,仍是真的要mock这个函数,由于一旦mock了这个函数,意味着隐藏了细节。
@Builder@Datapublic class UserVO { private String name; private int age; private Date birthday;}
public class UserVOFixture { // 注意:这里是个Supplier,并非一个静态的实例,这样能够保证每一个使用方,维护本身的实例 public static Supplier<UserVO.UserVOBuilder> DEFAULT_BUILDER = () -> UserVO.builder().name("test").age(11).birthday(new Date());}
(b)数据文件
public class UserVOFixture {
public static UserVO readUser(String filename) { return readJsonFromResource(filename, UserVO.class); }
public static <T> T readJsonFromResource(String filename, Class<T> clazz) { try { String jsonString = StreamUtils.copyToString(new ClassPathResource(filename).getInputStream(), Charset.defaultCharset()); return JSON.parseObject(jsonString, clazz); } catch (IOException e) { return null; } }}
FSC自己会给测试带来复杂度,而UnitTest应该简单,若是UnitTest自己都很复杂了,项目带来难以估量的测试成本。
Fixture其实能够在任何场景中使用,由于是底层的复用。
增长了代码复杂度。
经过IDE工具没法直接定位的测试文件,折衷的方案是case的命名符合ResouceTest的命名。
刻意练习,简而言之,就是刻意的练习,它突出的是有目的的练习。刻意练习也有它的一整套过程,在这个过程里,你须要遵照它的3F法则:
第一,Focus(保持专一)。
第二,Feedback(注重反馈,收集信息)。
第三,Fix it(纠正错误,而且进行修改)。
UT自己是一项技术,是须要咱们打磨、练习的,最好的练习方式,就是刻意练习,若是有决心,一个周末在家刻意练习,为项目中的部分场景加上UT,相信收获会很丰富。
应不该该连平常环境进行测试?
我的不建议直接连平常环境进行测试,若是两我的同时在跑测试,那么颇有可能测试环境的数据会处于混乱状态。并且UT尽量不要依赖过多的外部环境,依赖越多越复杂。测试仍是简单点好。
一个类里面测试太多怎么办?
考虑按测试的case区分,也可按测试的方法区分,也能够按正常、异常场景区分。
不知作别人mock了哪些数据怎么办?
尽可能让你们Mock数据的命名规范,经过Fixutre的复用,来减小新写测试的成本。
测试结构太复杂?
考虑是否是本身应用的代码组织就有问题?
测试莫名奇妙起不来?
须要详细了解JUNIT、Spring、PandoraBoot等是如何进行测试环境的mock的,是否是测试间的数据冲突等。详细的咱们会在方法篇持续更新,遇到问题解决问题。
不熟悉单元测试写法,尽可能写简单的单元测试,覆盖核心方法。
熟悉单元测试,业务复杂,覆盖正常、通常异常场景,另外对核心业务逻辑要有单独的测试。
DEBUG:阿里如今的基础设施是真的完善,中间件、各类监控、日志,只要系统埋点够好,遇到的不少问题均可以解决,即便有一些复杂问题,也能够local debug。但在一些特殊场景下,将数据MOCK好,利用UT来DEBUG,可能效率更高,你们能够试试。
测试如文档:咱们如今开发有不少完善的文档,但文档这东西和代码上毕竟有一层映射关系,若是能快速了解业务,完善的测试,有时候也是个不错的选择,例如你们学习一些开源框架的时候,都会从测试开始看。
重构:当你想下定决心重构的时候,才发现项目中没有单元测试,什么心情?
最后
若是你们对于单元测试有好的实践,或者对文章中的一些观点有些共鸣,你们能够在评论区留言,咱们互相学习一下。你们也能够在评论区写出本身的场景,你们一块儿探讨如何针对特定场景来实践。
相关连接
[1]https://martinfowler.com/bliki/TestPyramid.html
[2]https://martinfowler.com/articles/practical-test-pyramid.html
阿里云开发者成长计划来啦!面向整年龄段开发者提供免费云服务器、学习成长路线及场景体验实践,全面帮助开发者轻松掌握云上技能,助推成长,培养数字经济时代的云计算技术人才!
识别下方二维码,或点击 “阅读原文” ,快去参与吧~

本文分享自微信公众号 - 阿里巴巴技术质量(AlibabaTechQA)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。