不要仅仅依靠单元测试

当您构建一个复杂的系统时,仅仅测试组件是不够的。 这很关键,但还不够。 想象一下一家汽车厂生产并进口最高质量的零件,但组装好之后再也不会启动发动机了。 如果您的测试用例套件几乎不包含单元测试,则您将永远无法确保系统整体正常运行。 让我们举一个人为的例子:

public class UserDao {
 
    public List<User> findRecentUsers() {
        try {
            return //run some query
        } catch(EmptyResultDataAccessException ignored) {
            return null;
        }
    }
 
    //...
}

我希望您已经在catch块中发现了一个反模式(而且我并不是想忽略异常,这似乎是可以预期的)。 作为好公民,我们决定通过以下方式解决问题: 返回一个空集合,而不是null

public class UserDao {
 
    public List<User> findRecentUsers() {
        try {
            return //run some query
        } catch(EmptyResultDataAccessException ignored) {
            return Collections.emptyList();
        }
    }
 
    //...
}

修复非常简单,我们几乎忘记了运行单元测试,但是以防万一我们执行它们并发现第一个失败的测试用例:

public class UserDaoTest {
 
    private UserDao userDao;
 
    @Before
    public void setUp() throws Exception {
        userDao = new UserDao();
    }
 
    @Test
    public void shouldReturnNullWhenNoRecentUsers() throws Exception {
        //given
 
        //when
        final List<User> result = userDao.findRecentUsers();
 
        //then
        assertThat(result).isNull();
    }
 
    @Test
    public void shouldReturnOneRecentUser() throws Exception {
        //given
        final User lastUser = new User();
        userDao.storeLoginEvent(lastUser);
 
        //when
        final List<User> result = userDao.findRecentUsers();
 
        //then
        assertThat(result).containsExactly(lastUser);
    }
 
    @Test
    public void shouldReturnTwoRecentUsers() throws Exception {
        //given
        final User lastUser = new User();
        final User oneButLastUser = new User();
        userDao.storeLoginEvent(oneButLastUser);
        userDao.storeLoginEvent(lastUser);
 
        //when
        final List<User> result = userDao.findRecentUsers();
 
        //then
        assertThat(result).containsExactly(lastUser, oneButLastUser);
    }
 
}

显然,不仅代码被破坏了(通过返回null而不是像null的空集合),而且还进行了一项测试来验证这种虚假行为。 我很确定测试是在实现之后编写的,并且必须以某种方式处理现实。 在没有实施特性的事先知识的情况下,没有人会编写这样的测试。 因此,我们修复了测试,并乐意等待绿色CI的建立–最终来了。 几天后,我们的应用程序在生产中因NullPointerException中断。 它打破了经过彻底的单元测试的地方:

public class StatService {
 
    private final UserDao userDao;
 
    public StatService(UserDao userDao) {
        this.userDao = userDao;
    }
 
    public void welcomeMostRecentUser() {
        final List<User> recentUsers = userDao.findRecentUsers();
        if (recentUsers != null) {
            welcome(recentUsers.get(0));
        }
    }
 
    private void welcome(User user) {
        //...
    }
}

我们很惊讶,因为此类已被单元测试完全覆盖(为清楚起见,省略了验证步骤):

@RunWith(MockitoJUnitRunner.class)
public class WelcomeServiceTest {
 
    @Mock
    private UserDao userDaoMock;
    private WelcomeService welcomeService;
 
    @Before
    public void setup() {
        welcomeService = new WelcomeService(userDaoMock);
    }
 
    @Test
    public void shouldNotSendWelcomeMessageIfNoRecentUsers() throws Exception {
        //given
        given(userDaoMock.findRecentUsers()).willReturn(null);
 
        //when
        welcomeService.welcomeMostRecentUser();
 
        //then
        //verify no message sent
    }
 
    @Test
    public void shouldSendWelcomeMessageToMostRecentUser() throws Exception {
        //given
        given(userDaoMock.findRecentUsers()).willReturn(asList(new User()));
 
        //when
        welcomeService.welcomeMostRecentUser();
 
        //then
        //verify user welcomed
    }
 
    //...
 
}

您知道问题出在哪里吗? 我们更改了UserDao类的合同,同时使它在表面上“看起来”相同。 通过修复损坏的测试,我们认为它仍然可以工作。 但是, WelcomeService仍然依赖UserDao的旧行为,该行为要么返回null ,要么返回具有至少一个元素的列表。 使用模拟框架记录了此行为,因此我们能够对单元中的WelcomeService进行单独测试。 换句话说,我们无法确保这两个组件仍然可以正常工作,我们仅对它们进行了单独测试。 回到我们的汽车隐喻–所有零件仍然可以放在一起(相同的合同),但是其中一个内部的行为与以前不同。 那么,到底出了什么问题? 这里至少存在四个问题,如果缓解了任何一个,这些都不会发生。

首先, UserDao的作者未能认识到返回null而空列表似乎更加直观。 这引出了一个问题:有没有之间的差异显著null和空集? 如果是,也许您正在尝试在单个返回值中“编码”太多信息? 如果没有,为什么还要增加API使用者的生活呢? 遍历空集合不需要任何额外的工作; 对可能为null collection进行迭代需要一个额外的条件。 WelcomeService作者也因假定null表示空集合而失败。 他应该解决丑陋的API,而不要依赖它。 在这种情况下,他本可以使用CollectionUtils.isNotEmpty()并更具防御性:

if (CollectionUtils.isNotEmpty(recentUsers)) {

对于更全面的解决方案,他还可以考虑装饰 UserDao并将null替换为空collection。 甚至使用AOP在整个应用程序中全局修复此类API。 顺便说一句,这也适用于String 。 在99%的情况下, null ,空字符串和很少有空格的字符串之间没有“业务”差异。 除非您真的想区分它们,否则默认情况下使用StringUtils.isBlank()或类似名称。

最终,“修复” UserDao的人看不到大图。 仅仅修复单元测试是不够的。 当您在不更改API的情况下更改类的行为时(这对于动态语言尤为重要),您很可能会错过使用该API的地方,从而失去上下文。 但是最大的失败是缺少组件/系统测试 。 如果仅使用一个同时运行WelcomeService UserDao ,就会发现此错误。 仅有100%的代码覆盖率是不够的。 您测试了拼图的每一个部分,但从未看过完成的图片。 至少进行一些较大的烟雾测试。 否则,您将不再具有如此强大的信心,即当测试呈绿色时,代码就可以使用了。

参考: 不要单靠我们的JCG合作伙伴 Tomasz Nurkiewicz的NoBlogDefFound博客进行单元测试

翻译自: https://www.javacodegeeks.com/2013/02/dont-rely-on-unit-tests-alone.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值