怎样编写测试类测试分支_编写干净的测试–天堂中的麻烦

本文探讨了编写单元测试时可能遇到的问题,包括命名标准的局限性和通用配置的不足。作者指出,良好的单元测试应仅测试一件事、有明确的命名以及模拟外部依赖。然而,实际应用中,过长的测试方法名、重复的配置代码和不易维护的测试结构成为挑战。文章强调,编写简洁、易于理解和维护的单元测试至关重要。
摘要由CSDN通过智能技术生成

怎样编写测试类测试分支

如果我们的代码有明显的错误,我们很有动力对其进行改进。 但是,在某些时候,我们认为我们的代码“足够好”并继续前进。

通常,当我们认为改进现有代码的好处小于所需的工作时,就会发生这种情况。 当然,如果我们低估了投资回报,我们可能会打错电话,这会伤害我们。

这就是发生在我身上的事情,因此我决定写这篇文章,以便您避免犯同样的错误。

编写“良好”单元测试

如果我们要编写“好的”单元测试,则必须编写以下单元测试:

  • 只测试一件事 。 好的单元测试只能因一个原因而失败,并且只能断言一件事。
  • 被正确命名 。 测试方法的名称必须揭示测试失败的原因。
  • 模拟外部依赖关系(和状态) 。 如果单元测试失败,我们将确切知道问题出在哪里。

补充阅读:

如果我们编写满足这些条件的单元测试,我们将编写好的单元测试。 对?

我曾经这样认为。 现在我对此表示怀疑

善意铺平地狱之路

我从未见过一个决定编写糟糕的单元测试的软件开发人员。 如果开发人员正在编写单元测试,则他/她很有可能要编写好的单元测试。 但是,这并不意味着该开发人员编写的单元测试是好的。

我想编写既易于阅读又易于维护的单元测试。 我什至写了一个教程,描述了如何编写干净的测试 。 问题在于,本教程中给出的建议还不够好(尚未)。 它可以帮助我们入门,但是并没有显示出兔子洞的真正深度。

我的教程中描述的方法存在两个主要问题:

命名标准FTW?

如果我们使用Roy Osherove引入“命名标准” ,我们注意到很难描述被测状态和预期行为。

当我们为简单场景编写测试时,此命名标准非常有效。 问题在于,真正的软件并不简单。 通常,我们最终使用以下两个选项之一来命名测试方法:

首先 ,如果我们尝试尽可能具体,那么我们的测试方法的方法名称就显得太过looooooooong。 最后,我们必须承认我们不能像我们想要的那样具体,因为方法名称会占用太多空间。

其次 ,如果我们尝试使方法名称尽可能短,则方法名称将不会真正描述测试状态和预期行为。

选择哪个选项并不重要,因为无论如何我们都会遇到以下问题:

  • 如果测试失败,则方法名称不一定表示要出错。 我们可以使用自定义断言来解决此问题,但是它们不是免费的。
  • 很难对我们的测试涵盖的场景进行简要概述。

这是我们在“ 编写干净测试”教程期间编写的测试方法的名称:

  • registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldThrowException()
  • registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount()
  • registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldSaveNewUserAccountAndSetSignInProvider()
  • registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount()
  • registerNewUserAccount_SocialSignInAnUniqueEmail_ShouldNotCreateEncodedPasswordForUser()

这些方法的名称不是很长,但是我们必须记住,编写这些单元测试是为了测试一种简单的注册方法。 当我使用这种命名约定为现实生活中的软件项目编写自动化测试时,最长的方法名称是我们最长的示例名称的两倍。

那不是很干净或可读。 我们可以做得更好

没有通用配置

在本教程中,我们使单元测试变得更好了 。 然而,他们仍然遭受这样的事实,即没有“自然的”方式在不同的单元测试之间共享配置。

这意味着我们的单元测试包含许多重复的代码,这些代码配置了我们的模拟对象并创建了在我们的单元测试中使用的其他对象。

同样,由于没有“自然”的方式表明某些常量仅与特定的测试方法相关,因此我们必须将所有常量添加到测试类的开头。

我们的测试类的源代码如下(突出显示有问题的代码):

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.security.crypto.password.PasswordEncoder;
 
import static com.googlecode.catchexception.CatchException.catchException;
import static com.googlecode.catchexception.CatchException.caughtException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
 
@RunWith(MockitoJUnitRunner.class)
public class RepositoryUserServiceTest {
 
    private static final String REGISTRATION_EMAIL_ADDRESS = "john.smith@gmail.com";
    private static final String REGISTRATION_FIRST_NAME = "John";
    private static final String REGISTRATION_LAST_NAME = "Smith";
    private static final SocialMediaService SOCIAL_SIGN_IN_PROVIDER = SocialMediaService.TWITTER;
 
    private RepositoryUserService registrationService;
 
    @Mock
    private PasswordEncoder passwordEncoder;
 
    @Mock
    private UserRepository repository;
 
    @Before
    public void setUp() {
        registrationService = new RepositoryUserService(passwordEncoder, repository);
    }
 
    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldThrowException() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();
 
        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());
 
        catchException(registrationService).registerNewUserAccount(registration);
 
        assertThat(caughtException()).isExactlyInstanceOf(DuplicateEmailException.class);
    }
 
    @Test
    public void registerNewUserAccount_SocialSignInAndDuplicateEmail_ShouldNotSaveNewUserAccount() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();
 
        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(new User());
 
        catchException(registrationService).registerNewUserAccount(registration);
 
        verify(repository, never()).save(isA(User.class));
    }
 
    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_
ShouldSaveNewUserAccountAndSetSignInProvider() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();
 
        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
 
        registrationService.registerNewUserAccount(registration);
 
        ArgumentCaptor<User> userAccountArgument = ArgumentCaptor.forClass(User.class);
        verify(repository, times(1)).save(userAccountArgument.capture());
 
        User createdUserAccount = userAccountArgument.getValue();
 
        assertThatUser(createdUserAccount)
                .hasEmail(REGISTRATION_EMAIL_ADDRESS)
                .hasFirstName(REGISTRATION_FIRST_NAME)
                .hasLastName(REGISTRATION_LAST_NAME)
                .isRegisteredUser()
                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
    }
 
 
    @Test
    public void registerNewUserAccount_SocialSignInAndUniqueEmail_ShouldReturnCreatedUserAccount() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();
 
        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
 
        when(repository.save(isA(User.class))).thenAnswer(new Answer<User>() {
            @Override
            public User answer(InvocationOnMock invocation) throws Throwable {
                Object[] arguments = invocation.getArguments();
                return (User) arguments[0];
            }
        });
 
        User createdUserAccount = registrationService.registerNewUserAccount(registration);
 
        assertThatUser(createdUserAccount)
                .hasEmail(REGISTRATION_EMAIL_ADDRESS)
                .hasFirstName(REGISTRATION_FIRST_NAME)
                .hasLastName(REGISTRATION_LAST_NAME)
                .isRegisteredUser()
                .isRegisteredByUsingSignInProvider(SOCIAL_SIGN_IN_PROVIDER);
    }
 
    @Test
    public void registerNewUserAccount_SocialSignInAnUniqueEmail_ShouldNotCreateEncodedPasswordForUser() throws DuplicateEmailException {
        RegistrationForm registration = new RegistrationFormBuilder()
                .email(REGISTRATION_EMAIL_ADDRESS)
                .firstName(REGISTRATION_FIRST_NAME)
                .lastName(REGISTRATION_LAST_NAME)
                .isSocialSignInViaSignInProvider(SOCIAL_SIGN_IN_PROVIDER)
                .build();
 
        when(repository.findByEmail(REGISTRATION_EMAIL_ADDRESS)).thenReturn(null);
 
        registrationService.registerNewUserAccount(registration);
 
        verifyZeroInteractions(passwordEncoder);
    }
}

一些开发人员认为看起来像上面示例的单元测试足够干净。 我理解这种情绪,因为我曾经是其中之一。 但是,这些单元测试有三个问题:

  1. 该案的实质并不尽如人意 。 因为每种测试方法在调用被测试方法并验证预期结果之前都会进行自我配置,所以我们的测试方法变得比必要的更长。 这意味着我们不能只看一眼随机测试方法并弄清楚它要测试什么。
  2. 编写新的单元测试很慢 。 因为每个单元测试都必须自行配置,所以向我们的测试套件中添加新的单元测试比它可能要慢得多。 另一个“意外”的缺点是,这种单元测试鼓励人们练习复制和粘贴编程
  3. 维持这些单元测试是一件痛苦的事 。 如果我们向注册表单添加新的必填字段,或者更改registerNewUserAccount()方法的实现,则必须对每个单元测试进行更改。 这些单元测试太脆弱了。

换句话说,这些单元测试很难阅读,很难编写和维护。 我们必须做得更好

摘要

这篇博客文章教会了我们四件事:

  • 即使我们认为我们正在编写好的单元测试,也不一定是正确的。
  • 如果由于必须更改许多单元测试而导致更改现有功能的速度很慢,那么我们就不会编写好的单元测试。
  • 如果添加新功能的速度很慢,因为我们必须向单元测试中添加大量重复的代码,那么我们就不会编写好的单元测试。
  • 如果我们看不到单元测试所涵盖的情况,那么我们就没有编写好的单元测试。

本教程的下一部分将回答这个非常相关的问题:

如果我们现有的单元测试很烂,我们该如何解决呢?

如果要编写干净的测试,则应阅读我的“ 编写干净的测试”教程

翻译自: https://www.javacodegeeks.com/2015/03/writing-clean-tests-trouble-in-paradise.html

怎样编写测试类测试分支

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值