阿里妹导读:测试不应该是一门很高大尚的技术,应该是我们技术人的基本功。但现在好像慢慢地,单元测试已经脱离了基本功的范畴。笔者曾经在不同团队中推过单元测试,要求过覆盖率,但发现实施下去很难。后来在不停地刻意练习后,发现阻碍写UT的只是笔者的心魔,并不是时间和项目的问题。在经过一些项目的实践后,也是有了一些自己的理解和实践,希望和大家分享一下,和大家探讨下如何克服“单元测试”的心魔。
文末福利:开发者成长计划,最强助力!
内功
前人们在单元测试方面的研究很多,有很多的方法论,我们可以拿来即用。我简单介绍两个方法论,一个概念。希望大家可以查阅更多的资料,凝聚自己的内功心法。
TDD
TestDrivenDevelopment,也被认为是TestDrivenDesign,我们这里按第一种定义来聊。TDD一改以往的破坏性测试的思维方式,测试在先、编码在后,更符合“缺陷预防”的思想。简单来说,TDD的流程是“红-绿-重构”三个步骤的循环往复。
红:测试先行,现在还没有任何实现,跑UT的时候肯定不过,测试状态是红灯。编译失败也属于“红”的一种情况。
绿:当我们用最快,最简单的方式先实现,然后跑一遍UT,测试会通过,变成“绿”的状态。
重构:看一下系统中有没有要重构的点,重构完,一定要保证测试是“绿”的。
业界有很多TDD的呼声,也有TDD已死的文章。方法本来没有对错,只有优劣,我们要辩证地来看。只能说TDD不是一个银弹,不能解决所有问题。以笔者自己的经验,TDD比较适用于输入输出很明确的CASE,很多时候我们在摸索一种新的模式的时候,可能并不太适用。
如果你和前端已经商议好了接口的出参、入参,可以尝试一下TDD,一种新的思路,新的思想。
BDD
严格来说BDD是TDD衍生出来的一个小分支。但也可以用于一些不同维度的东西。概念大家自行寻找资料。这里讲一下BDD的一点实践经验。直接上代码:
RunWith(SpringBootRunner.class)DelegateTo(SpringJUnit4ClassRunner.class)SpringBootTest(classes={Application.class})publicclassApiServiceTest{AutowiredApiServiceapiService;TestpublicvoidtestMobileRegister(){AlispResultMapString,Objectresult=apiService.mobileRegister();System.out.println(result=+result);Assert.assertNotNull(result);Assert.assertEquals(54,result.getAlispCode().longValue());AlispResultMapString,Objectresult2=apiService.mobileRegister();System.out.println(result2=+result2);Assert.assertNotNull(result2);Assert.assertEquals(9,result2.getAlispCode().longValue());AlispResultMapString,Objectresult3=apiService.mobileRegister();System.out.println(result3=+result3);Assert.assertNotNull(result3);Assert.assertEquals(,result3.getAlispCode().longValue());}Testpublicvoidshould_return_mobile_is_not_correct_when_register_given_a_invalid_phone_number(){AlispResultMapString,Objectresult=apiService.mobileRegister();Assert.assertNotNull(result);Assert.assertFalse(result.isSuccess());}}第一个UT是以方法维度,把所有场景放到一个方法来测试。
第二个UT是以case为角度,针对每个case单独的测试。
其实TDD里面有一个概念是隔离性,单元测试之间应该隔离开,不要互相干扰。另外,从命名上,第二种也更好一点。我个人还是比较推荐以下命名方式的:
should:返回值,应该产生的结果when:哪个方法given:哪个场景
另外BDD或者TDD中也有Task的概念,写代码之前先准备好case。大家可以看一些BDD的文章,自己体会。如果对这个感兴趣,可以在评论区探讨。
测试金字塔
上图来自martinfowler博客的TestPyramid[1]一文,也可以读一下《PracticalTestPyramid》[2]。特别棒的文章,希望大家可以去读一读。
上面的金字塔的意思是,从Unit到Service,再到UI,速度越来越慢,成本也越来越高。
我们可以从服务端的角度把这三层稍微改一下:
契约测试:测试服务与服务之间的契约,接口保证。代价最高,测试速度最慢。
集成测试(Integration):集成当前spring容器、中间件等,对服务内的接口,或者其他依赖于环境的方法的测试。
//加载spring环境
RunWith(SpringBootRunner.class)DelegateTo(SpringJUnit4ClassRunner.class)SpringBootTest(classes={Application.class})publicclassApiServiceTest{AutowiredApiServiceapiService;//dosometest}单元测试(UnitTest):纯函数,方法的测试,不依赖于spring容器,也不依赖于其他的环境。
我们现在写测试,一般是单元测试和集成测试两层。针对具体场景,选择适合自己的测试粒度。
招数
其实写单元测试是有一些招数的,下面会介绍笔者很喜欢的一种单元测试代码组织结构,也会介绍一些常用的招数,以及使用场景。
常见问题
一个类里面测试太多怎么办?不知道别人mock了哪些数据怎么办?测试结构太复杂?测试莫名奇妙起不来?
Fixture-Scenario-Case
FSC(Fixture-Scenario-Case)是一种组织测试代码的方法,目标是尽量将一些MOCK信息在不同的测试中共享。其结构如下:
通过组合Fixture(固定设施),来构造一个Scenario(场景)。
通过组合Scenario(场景)+Fixture(固定设施),构造一个case(用例)。
下面是一个FSC的示例:
Case:当用户正常登录后,获取当前登录信息时,应该返回正确的用户信息。这是一个简单的用户登录的case,这个case里面总共有两个动作、场景,一个是用户正常登录,一个是获取用户信息,演化为两个scenario。
Scenario:用户正常登录,肯定需要登录参数,如:手机号、验证码等,另外隐含着数据库中应该有一个对应的用户,如果登录时需要与第三方系统进行交互,还需要对第三方系统进行mock或者stub。获取用户信息时,肯定需要上一阶段颁发的凭证信息,另外该凭证可能是存储于一些缓存系统的,所以还需要对中间件进行mock或者stub。
Fixture
利用Builder模式构造请求参数。
利用DataFile来存储构造用户的信息,例如DBtransaction进行数据的存储和隔离。
利用Mockito进行三方系统、中间件的Mock。
当这样组织测试时,如果另外一个Case中需要用户登录,则可以直接复用用户登录的Scenario。也可以通过复用Fixture来减少数据的Mock。下面我们来详细解释看一下每一层如何实现,showthecode。
Case
case是用例的意思,在这里用例是场景和一些固定设施的组合。这里要注意的是,尽量不要直接修改接口的数据,一个场景所依赖的环境应该是另一个场景的输出。当然有些特定场景下,还是需要直接改数据的,这里不是禁止,而是建议。
publicclassGetUserInfoCaseextendsBaseTest{privateStringaccessToken;
AutowiredprivateUserFixtureuserFixture;/***通用场景的mock*/BeforepublicvoidsetUp(){//三方系统mockuserFixture.whenFetchUserInfoThenReturn(1,newUserVO());//依赖的其他场景accessToken=newSimpleLoginScenario().mobile().code(aaa).login().getAccessToken();}/***BDD的三段式*/Testpublicvoidshould_return_user_info_when_user_login_given_a_effective_access_token(){ResponseuserInfoResponse=newGetUserInfoScenario().accessToken(accessToken).getUserInfo();assertThat(userInfoResponse.jsonPath().getString(id),equals(1));}}Scenario
JUNIT的用法就不说了,相信大家都了解,这里提两个框架RESTAssured和MockMVC。这两个框架都可以用来做接口测试,MockMVC是spring原生的,可以指定加载的Resource,一定程度上可以提升UT速度,但是和spring是耦合在一起的。RESTAssured是脱离Spring的,可以理解为利用