从工作中清除代码–使用JUnit 5,Mockito和AssertJ编写可执行规范

可执行规范是可以用作设计规范的测试。 通过启用公共语言(在DDD世界中,这也称为无处不在的语言 ),它们使技术和业务团队能够进入同一页面。 它们充当代码的未来维护者的文档。
在本文中,我们将看到一种编写自动测试的自以为是的方式,该方法也可以用作可执行规范。

让我们从一个例子开始。 假设我们正在为企业创建会计系统。 该系统将允许其用户将收入和支出记录到不同的帐户中。 在用户开始记录收入和支出之前,他们应该能够在系统中添加新帐户。 假设“添加新帐户”用例的规范如下所示–

场景1

给定帐户不存在 用户添加新帐户时 然后添加的帐户具有给定的名称 然后添加的帐户具有给定的初始余额 然后添加的帐户具有用户的ID

方案2

给定帐户不存在 当用户添加初始余额为负的新帐户时 然后添加新帐户失败

情况3

具有相同名称的给定帐户 用户添加新帐户时 然后添加新帐户失败

为了创建一个新帐户,用户需要在系统中输入一个帐户名和一个初始余额。 如果不存在具有给定名称的帐户并且给定的初始余额为正,则系统将创建该帐户。

我们将首先写下一个测试,该测试将捕获第一个场景的第一个“ Given-When-Then”部分。 这就是它的样子–

 class AddNewAccountTest { @Test @DisplayName ( "Given account does not exist When user adds a new account Then added account has the given name" ) void accountAddedWithGivenName() {     }  } 

@DisplayName批注是在JUnit 5中引入的。它为测试分配了易于理解的名称。 这是我们执行此测试时看到的标签,例如在像IntelliJ IDEA这样的IDE中。

现在,我们将创建一个类,负责添加帐户

 class AddNewAccountService { void addNewAccount(String accountName) { }  } 

该类定义单个方法,该方法接受帐户名称并负责创建帐户,即将其保存到持久数据存储中。 由于我们决定将此类称为AddNewAccountService,因此我们还将测试重命名为AddNewAccountServiceTest以遵循JUnit世界中使用的命名约定。

现在,我们可以继续编写测试了–

 class AddNewAccountServiceTest { @Test @DisplayName ( "Given account does not exist When user adds a new account Then added account has the given name" ) void accountAddedWithGivenName() { AddNewAccountService accountService = new AddNewAccountService(); accountService.addNewAccount( "test account" );     // What to test? }  } 

我们应该测试/验证什么以确保正确实施该方案? 如果再次阅读我们的规范,很显然,我们想创建一个用户指定名称的“帐户”,因此我们应该在此处进行测试。 为此,我们必须首先创建一个代表帐户的类-

 @AllArgsConstructor  class Account { private String name;  } 

Account类只有一个名为name的属性。 它将具有其他字段,例如用户ID和余额,但是我们目前尚未测试它们,因此我们不会立即将它们添加到类中。

现在,我们已经创建了Account类,我们如何保存它,更重要的是,我们如何测试所保存的帐户具有用户指定的名称? 有许多方法可以做到这一点,而我的首选方法是定义一个接口,该接口将封装此保存操作。 让我们继续创建它–

 interface SaveAccountPort { void saveAccount(Account account);  } 

AddNewAccountService将通过构造函数注入注入该接口的实现–

 @RequiredArgsConstructor  class AddNewAccountService { private final SaveAccountPort saveAccountPort; void addNewAccount(String accountName) { }  } 

为了进行测试,我们将在Mockito的帮助下创建一个模拟实现,这样我们就不必担心实际的实现细节了–

 @ExtendWith (MockitoExtension. class )  class AddNewAccountServiceTest { @Mock private SaveAccountPort saveAccountPort; @Test @DisplayName ( "Given account does not exist When user adds a new account Then added account has the given name" ) void accountAddedWithGivenName() { AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); accountService.addNewAccount( "test account" );     // What to test? }  } 

我们的测试设置现已完成。 现在,我们希望我们的测试方法(AddNewAccountService类的addNewAccount方法)调用SaveAccountPort的saveAccount方法,并将Account对象的名称设置为传递给该方法的对象。 让我们在测试中将其整理成句–

 @ExtendWith (MockitoExtension. class )  class AddNewAccountServiceTest { @Mock private SaveAccountPort saveAccountPort; @Captor private ArgumentCaptor<Account> accountArgumentCaptor; @Test @DisplayName ( "Given account does not exist When user adds a new account Then added account has the given name" ) void accountAddedWithGivenName() { AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); accountService.addNewAccount( "test account" ); BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); BDDAssertions.then(accountArgumentCaptor.getValue().getName()).isEqualTo( "test account" ); }  } 

下面的行–

 BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); 

验证一旦调用了被测试方法,即已调用SaveAccountPort的saveAccount方法。 我们还使用参数捕获器捕获传递到saveAccount方法的帐户参数。 下一行–

 BDDAssertions.then(accountArgumentCaptor.getValue().getName()).isEqualTo( "test account" ); 

然后验证捕获的帐户参数与测试中通过的名称相同。

为了使此测试通过,在我们的被测方法中需要的最少代码如下:

 @RequiredArgsConstructor  class AddNewAccountService { private final SaveAccountPort saveAccountPort; void addNewAccount(String accountName) { saveAccountPort.saveAccount( new Account(accountName)); }  } 

这样,我们的测试开始通过!

让我们继续进行第一个方案的第二个“ Then”部分,它说–

然后添加的帐户具有给定的初始余额

让我们编写另一个测试来验证这一部分–

 @Test  @DisplayName ( "Given account does not exist When user adds a new account Then added account has the given initial balance" )  void accountAddedWithGivenInitialBalance() { AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); accountService.addNewAccount( "test account" , "56.0" );   BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); BDDAssertions.then(accountArgumentCaptor.getValue().getBalance()) .isEqualTo( new BigDecimal( "56.0" ));  } 

我们修改了addNewAccount方法以接受初始余额作为第二个参数。 我们还在帐户对象中添加了一个称为余额的新字段,该字段可以存储帐户余额–

 @AllArgsConstructor  @Getter  class Account { private String name; private BigDecimal balance;  } 

由于我们更改了addNewAccount方法的签名,因此我们还必须修改我们的第一个测试–

 @Test  @DisplayName ( "Given account does not exist When user adds a new account Then added account has the given name" )  void accountAddedWithGivenName() { AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); accountService.addNewAccount( "test account" , "1" ); BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); BDDAssertions.then(accountArgumentCaptor.getValue().getName()).isEqualTo( "test account" );  } 

如果我们现在运行新的测试,它将由于我们尚未实现的功能而失败。 现在就开始吧–

 void addNewAccount(String accountName, String initialBalance) { saveAccountPort.saveAccount( new Account(accountName, new BigDecimal(initialBalance)));  } 

我们的两个测试现在都应该通过。

由于我们已经进行了一些测试,现在该看看我们的实现,看看是否可以做得更好。 由于我们的AddNewAccountService非常简单,因此我们无需在此做任何事情。 对于我们的测试,我们可以消除测试设置代码中的重复项–两个测试都实例化AddNewAccountService的实例,并以相同的方式在其上调用addNewAccount方法。 是删除还是保留重复项取决于我们的测试编写方式-如果我们想使每个测试尽可能独立,那么就让它们保持原样。 但是,如果我们有通用的测试设置代码是可以的,那么我们可以按以下方式更改测试

 @ExtendWith (MockitoExtension. class )  @DisplayName ( "Given account does not exist When user adds a new account" )  class AddNewAccountServiceTest { private static final String ACCOUNT_NAME = "test account" ; private static final String INITIAL_BALANCE = "56.0" ; @Mock private SaveAccountPort saveAccountPort; @Captor private ArgumentCaptor<Account> accountArgumentCaptor; @BeforeEach void setup() { AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); accountService.addNewAccount(ACCOUNT_NAME, INITIAL_BALANCE); } @Test @DisplayName ( "Then added account has the given name" ) void accountAddedWithGivenName() { BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); BDDAssertions.then(accountArgumentCaptor.getValue().getName()).isEqualTo(ACCOUNT_NAME); } @Test @DisplayName ( "Then added account has the given initial balance" ) void accountAddedWithGivenInitialBalance() { BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); BDDAssertions.then(accountArgumentCaptor.getValue().getBalance()) .isEqualTo( new BigDecimal(INITIAL_BALANCE)); }  } 

请注意,我们还提取了@DisplayName的公共部分,并将其放在测试类的顶部。 如果我们不愿意这样做,我们也可以保持原样。

由于我们有多个通过的测试,因此从现在开始,每一次失败的测试通过,我们都会停一会儿,看看我们的实现,并尝试对其进行改进。 总而言之,我们的实施过程现在将包括以下步骤-

  1. 在确保现有测试持续通过的同时添加失败的测试
  2. 通过失败的测试
  3. 暂停片刻,然后尝试改善实施(代码和测试)

继续,我们现在需要使用创建的帐户存储用户ID。 按照我们的方法,我们将首先编写一个失败的测试以捕获此错误,然后添加使失败的测试通过的最少代码量。 一旦失败的测试开始通过,这就是实现的样子

 @ExtendWith (MockitoExtension. class )  @DisplayName ( "Given account does not exist When user adds a new account" )  class AddNewAccountServiceTest { private static final String ACCOUNT_NAME = "test account" ; private static final String INITIAL_BALANCE = "56.0" ; private static final String USER_ID = "some id" ; private Account savedAccount; @BeforeEach void setup() { AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); accountService.addNewAccount(ACCOUNT_NAME, INITIAL_BALANCE, USER_ID); BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); savedAccount = accountArgumentCaptor.getValue(); }   // Other tests..... @Test @DisplayName ( "Then added account has user's id" ) void accountAddedWithUsersId() { BDDAssertions.then(accountArgumentCaptor.getValue().getUserId()).isEqualTo(USER_ID); }  }  @RequiredArgsConstructor  class AddNewAccountService { private final SaveAccountPort saveAccountPort; void addNewAccount(String accountName, String initialBalance, String userId) { saveAccountPort.saveAccount( new Account(accountName, new BigDecimal(initialBalance), userId)); }  }  @AllArgsConstructor  @Getter  class Account { private String name; private BigDecimal balance; private String userId;  } 

既然所有测试都通过了,那就是改进的时间了! 注意,addNewAccount方法已经接受了三个参数。 随着我们引入越来越多的帐户属性,其参数列表也将开始增加。 我们可以引入一个参数对象来避免这种情况

 @RequiredArgsConstructor  class AddNewAccountService { private final SaveAccountPort saveAccountPort; void addNewAccount(AddNewAccountCommand command) { saveAccountPort.saveAccount( new Account( command.getAccountName(), new BigDecimal(command.getInitialBalance()), command.getUserId() ) ); } @Builder @Getter static class AddNewAccountCommand { private final String userId; private final String accountName; private final String initialBalance; }  }  @ExtendWith (MockitoExtension. class )  @DisplayName ( "Given account does not exist When user adds a new account" )  class AddNewAccountServiceTest { // Fields..... @BeforeEach void setup() { AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); AddNewAccountCommand command = AddNewAccountCommand.builder() .accountName(ACCOUNT_NAME) .initialBalance(INITIAL_BALANCE) .userId(USER_ID) .build(); accountService.addNewAccount(command); BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); savedAccount = accountArgumentCaptor.getValue(); } // Remaining Tests.....  } 

如果现在在我的IDEA中运行测试,这就是我所看到的–

当我们尝试在此视图中阅读测试描述时,我们已经可以很好地了解“添加新帐户”用例及其工作方式。

好的,让我们继续进行用例的第二种情况,这是一个验证规则

给定帐户不存在

当用户添加初始余额为负的新帐户时

然后添加新帐户失败

让我们编写一个新的测试来尝试捕获这一点–

 @ExtendWith (MockitoExtension. class )  @DisplayName ( "Given account does not exist When user adds a new account" )  class AddNewAccountServiceTest { // Other tests @Test @DisplayName ( "Given account does not exist When user adds a new account with negative initial balance Then add new account fails" ) void addNewAccountFailsWithNegativeInitialBalance() { AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); AddNewAccountCommand command = AddNewAccountCommand.builder().initialBalance( "-56.0" ).build(); AddNewAccountCommand command = AddNewAccountCommand.builder().initialBalance( ).build(); accountService.addNewAccount(command); BDDMockito.then(saveAccountPort).shouldHaveNoInteractions(); }  } 

我们可以通过几种方法在服务中实施验证。 我们可以抛出一个异常详细说明验证失败,或者可以返回一个包含错误详细信息的错误对象。 在此示例中,如果验证失败,我们将抛出异常–

 @Test  @DisplayName ( "Given account does not exist When user adds a new account with negative initial balance Then add new account fails" )  void addNewAccountFailsWithNegativeInitialBalance() { AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); AddNewAccountCommand command = AddNewAccountCommand.builder().initialBalance( "-56.0" ).build(); AddNewAccountCommand command = AddNewAccountCommand.builder().initialBalance( ).build(); assertThatExceptionOfType(IllegalArgumentException. class ) .isThrownBy(() -> accountService.addNewAccount(command)); BDDMockito.then(saveAccountPort).shouldHaveNoInteractions();  } 

此测试验证以负余额调用addNewAccount方法时是否引发异常。 它还确保在这种情况下,我们的代码不会调用SaveAccountPort的任何方法。 在我们开始修改我们的服务以通过此测试之前,我们必须重构一下我们的测试设置代码。 这是因为在我们之前的重构中,我们将通用测试设置代码移到了一个方法中,该方法现在可以在每次测试之前运行–

 @BeforeEach  void setup() { AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); AddNewAccountCommand command = AddNewAccountCommand.builder() .accountName(ACCOUNT_NAME) .initialBalance(INITIAL_BALANCE) .userId(USER_ID) .build(); accountService.addNewAccount(command); BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); savedAccount = accountArgumentCaptor.getValue();  } 

现在,此设置代码与我们刚刚添加的新测试直接冲突–在每次测试之前,它将始终使用有效的命令对象调用addNewAccount方法,从而导致调用SaveAccountPort的saveAccount方法,从而导致新测试失败。

为了解决这个问题,我们将在测试类中创建一个嵌套类,在其中我们将移动现有的设置代码和通过测试–

 @ExtendWith (MockitoExtension. class )  @DisplayName ( "Given account does not exist" )  class AddNewAccountServiceTest { @Mock private SaveAccountPort saveAccountPort; private AddNewAccountService accountService; @BeforeEach void setUp() { accountService = new AddNewAccountService(saveAccountPort); } @Nested @DisplayName ( "When user adds a new account" ) class WhenUserAddsANewAccount { private static final String ACCOUNT_NAME = "test account" ; private static final String INITIAL_BALANCE = "56.0" ; private static final String USER_ID = "some id" ; private Account savedAccount; @Captor private ArgumentCaptor<Account> accountArgumentCaptor; @BeforeEach void setUp() { AddNewAccountCommand command = AddNewAccountCommand.builder() .accountName(ACCOUNT_NAME) .initialBalance(INITIAL_BALANCE) .userId(USER_ID) .build(); accountService.addNewAccount(command); BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); savedAccount = accountArgumentCaptor.getValue(); } @Test @DisplayName ( "Then added account has the given name" ) void accountAddedWithGivenName() { BDDAssertions.then(savedAccount.getName()).isEqualTo(ACCOUNT_NAME); } @Test @DisplayName ( "Then added account has the given initial balance" ) void accountAddedWithGivenInitialBalance() { BDDAssertions.then(savedAccount.getBalance()).isEqualTo( new BigDecimal(INITIAL_BALANCE)); } @Test @DisplayName ( "Then added account has user's id" ) void accountAddedWithUsersId() { BDDAssertions.then(accountArgumentCaptor.getValue().getUserId()).isEqualTo(USER_ID); } }   @Test @DisplayName ( "When user adds a new account with negative initial balance Then add new account fails" ) void addNewAccountFailsWithNegativeInitialBalance() { AddNewAccountCommand command = AddNewAccountCommand.builder() .initialBalance( "-56.0" ) .build(); assertThatExceptionOfType(IllegalArgumentException. class ) .isThrownBy(() -> accountService.addNewAccount(command)); BDDMockito.then(saveAccountPort).shouldHaveNoInteractions(); }  } 

这是我们采取的重构步骤–

  1. 我们创建了一个内部类,然后用JUnit 5的@Nested批注标记内部类。
  2. 我们破坏了最外面的测试类的@DisplayName标签,并将“当用户添加新帐户时”部分移到了新引入的内部类中。 我们这样做的原因是因为此内部类将包含一组测试,这些测试将验证/验证与有效帐户创建方案有关的行为。
  3. 我们将相关的设置代码和字段/常量移到了这个内部类中。
  4. 我们从新测试中删除了“给定帐户不存在”部分。 这是因为最外层测试类上的@DisplayName已经包含了此内容,因此这里再也没有包含它。

现在是在IntelliJ IDEA中运行测试时的样子,

从屏幕截图中可以看到,我们的测试标签也按照我们在测试代码中创建的结构很好地进行了分组和缩进。 现在,让我们修改服务以使失败的测试通过–

 void addNewAccount(AddNewAccountCommand command) { BigDecimal initialBalance = new BigDecimal(command.getInitialBalance()); if (initialBalance.compareTo(BigDecimal.ZERO) < 0 ) { throw new IllegalArgumentException( "Initial balance of an account cannot be negative" ); } saveAccountPort.saveAccount( new Account( command.getAccountName(), initialBalance, command.getUserId() ) );  } 

这样,我们所有的测试再次开始通过。 下一步是寻找可能的方法来改进现有的实现。 如果没有,那么我们将继续执行最终方案,这也是一个验证规则–

具有相同名称的给定帐户

用户添加新帐户时

然后添加新帐户失败

和往常一样,让我们​​编写一个测试来捕获这一点–

 @Test  @DisplayName ( "Given account with the same name exists When user adds a new account Then add new account fails" )  void addNewAccountFailsForDuplicateAccounts() { AddNewAccountCommand command = AddNewAccountCommand.builder() .accountName( "existing name" ) .build(); AddNewAccountService accountService = new AddNewAccountService(saveAccountPort); assertThatExceptionOfType(IllegalArgumentException. class ) .isThrownBy(() -> accountService.addNewAccount(command)); BDDMockito.then(saveAccountPort).shouldHaveNoInteractions();  } 

我们现在必须弄清的第一件事是如何找到现有帐户。 由于这将涉及查询我们的持久数据存储,因此我们将引入一个接口–

 public interface FindAccountPort { Account findAccountByName(String accountName);  } 

并将其注入我们的AddNewAccountService –

 @RequiredArgsConstructor  class AddNewAccountService { private final SaveAccountPort saveAccountPort; private final FindAccountPort findAccountPort;   // Rest of the code  } 

并修改我们的测试–

 @Test  @DisplayName ( "Given account with the same name exists When user adds a new account Then add new account fails" )  void addNewAccountFailsForDuplicateAccounts() { String existingAccountName = "existing name" ; AddNewAccountCommand command = AddNewAccountCommand.builder() .initialBalance( "0" ) .accountName(existingAccountName) .build(); given(findAccountPort.findAccountByName(existingAccountName)).willReturn(mock(Account. class )); AddNewAccountService accountService = new AddNewAccountService(saveAccountPort, findAccountPort); assertThatExceptionOfType(IllegalArgumentException. class ) .isThrownBy(() -> accountService.addNewAccount(command)); BDDMockito.then(saveAccountPort).shouldHaveNoInteractions();  } 

对AddNewAccountService的最后更改也将需要对现有测试进行更改,主要是在我们实例化该类的实例的位置。 但是,我们将做的改变不止于此–

 @ExtendWith (MockitoExtension. class )  class AddNewAccountServiceTest { @Mock private SaveAccountPort saveAccountPort; @Mock private FindAccountPort findAccountPort; @Nested @DisplayName ( "Given account does not exist" ) class AccountDoesNotExist { private AddNewAccountService accountService; @BeforeEach void setUp() { accountService = new AddNewAccountService(saveAccountPort, findAccountPort); } @Nested @DisplayName ( "When user adds a new account" ) class WhenUserAddsANewAccount { private static final String ACCOUNT_NAME = "test account" ; private static final String INITIAL_BALANCE = "56.0" ; private static final String USER_ID = "some id" ; private Account savedAccount; @Captor private ArgumentCaptor<Account> accountArgumentCaptor; @BeforeEach void setUp() { AddNewAccountCommand command = AddNewAccountCommand.builder() .accountName(ACCOUNT_NAME) .initialBalance(INITIAL_BALANCE) .userId(USER_ID) .build(); accountService.addNewAccount(command); BDDMockito.then(saveAccountPort).should().saveAccount(accountArgumentCaptor.capture()); savedAccount = accountArgumentCaptor.getValue(); } @Test @DisplayName ( "Then added account has the given name" ) void accountAddedWithGivenName() { BDDAssertions.then(savedAccount.getName()).isEqualTo(ACCOUNT_NAME); } @Test @DisplayName ( "Then added account has the given initial balance" ) void accountAddedWithGivenInitialBalance() { BDDAssertions.then(savedAccount.getBalance()).isEqualTo( new BigDecimal(INITIAL_BALANCE)); } @Test @DisplayName ( "Then added account has user's id" ) void accountAddedWithUsersId() { BDDAssertions.then(accountArgumentCaptor.getValue().getUserId()).isEqualTo(USER_ID); } } @Test @DisplayName ( "When user adds a new account with negative initial balance Then add new account fails" ) void addNewAccountFailsWithNegativeInitialBalance() { AddNewAccountCommand command = AddNewAccountCommand.builder() .initialBalance( "-56.0" ) .build(); assertThatExceptionOfType(IllegalArgumentException. class ) .isThrownBy(() -> accountService.addNewAccount(command)); BDDMockito.then(saveAccountPort).shouldHaveNoInteractions(); } } @Test @DisplayName ( "Given account with the same name exists When user adds a new account Then add new account fails" ) void addNewAccountFailsForDuplicateAccounts() { String existingAccountName = "existing name" ; AddNewAccountCommand command = AddNewAccountCommand.builder() .initialBalance( "0" ) .accountName(existingAccountName) .build(); given(findAccountPort.findAccountByName(existingAccountName)).willReturn(mock(Account. class )); AddNewAccountService accountService = new AddNewAccountService(saveAccountPort, findAccountPort); assertThatExceptionOfType(IllegalArgumentException. class ) .isThrownBy(() -> accountService.addNewAccount(command)); BDDMockito.then(saveAccountPort).shouldHaveNoInteractions(); }  } 

这就是我们所做的–

  1. 我们创建了另一个内部类,将其标记为@Nested,然后将现有的通过测试移入其中。 这组测试测试在不存在具有给定名称的帐户时添加新帐户的行为。
  2. 我们已将测试设置代码移至新引入的内部类中,因为它们也与“不存在具有给定名称的帐户”的情况有关。
  3. 出于与上述相同的原因,我们还将@DisplayName注释从顶级测试类移至了新引入的内部类。

重构后,我们快速运行测试以查看一切是否按预期工作(测试失败,通过测试通过),然后继续修改我们的服务–

 @RequiredArgsConstructor  class AddNewAccountService { private final SaveAccountPort saveAccountPort; private final FindAccountPort findAccountPort; void addNewAccount(AddNewAccountCommand command) { BigDecimal initialBalance = new BigDecimal(command.getInitialBalance()); if (initialBalance.compareTo(BigDecimal.ZERO) < 0 ) { throw new IllegalArgumentException( "Initial balance of an account cannot be negative" ); } if (findAccountPort.findAccountByName(command.getAccountName()) != null ) { throw new IllegalArgumentException( "An account with given name already exists" ); } saveAccountPort.saveAccount( new Account( command.getAccountName(), initialBalance, command.getUserId() ) ); } @Builder @Getter static class AddNewAccountCommand { private final String userId; private final String accountName; private final String initialBalance; }  } 

我们所有的测试现在都是绿色的–

由于我们的用例实现现已完成,因此我们将最后一次查看实现,以查看是否可以改进任何东西。 如果没有,那么我们的用例实现现在就完成了!

总而言之,这就是我们在本文中所做的–

  1. 我们已经写下了要实现的用例
  2. 我们添加了一个失败的测试,并使用易于理解的名称进行标记
  3. 我们添加了使测试通过失败所需的最少代码量
  4. 一旦我们进行了一项以上的测试,在通过每一次失败的测试之后,我们查看了实现并试图对其进行改进
  5. 在编写测试时,我们尝试以某种方式编写测试,以使用例规范反映在测试代码中。 为此,我们使用了–
    1. @DisplayName批注为我们的测试分配易于理解的名称
    2. @Nested用于按层次结构将相关测试分组,以反映我们的用例设置
    3. 使用了Mockito和AssertJ的BDD驱动的API来验证预期的行为

我们什么时候应该遵循这种编写自动化测试的风格? 该问题的答案与软件工程中的所有其他用法问题相同-取决于情况。 当我使用具有复杂业务/域规则的应用程序时,我个人更喜欢这种样式,该规则需要长期维护,为此需要与业务部门紧密合作,以及许多其他因素(例如,应用程序)架构,团队采用率等)。

与往常一样,完整的示例已提交给Github 。

直到下一次!

翻译自: https://www.javacodegeeks.com/2020/04/clean-code-from-the-trenches-writing-executable-specifications-with-junit-5-mockito-and-assertj.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/339860.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

家用计算机注意哪些参数,电脑小白买内存条要注意哪些?主要看哪些参数?这些知识要掌握...

内存条是电脑的核心硬件之一&#xff0c;它的作用主要是为CPU服务的&#xff0c;电脑运行的时候&#xff0c;CPU从硬盘里调用数据通过总线寻址放在内存里&#xff0c;内存相当于缓冲处理区&#xff0c;处理好信息后再回馈给CPU&#xff0c;然后电脑再根据指令运行。内存没有记忆…

日照职业技术学院计算机怎么样,日照职业技术学院宿舍条件怎么样 住宿环境好不好...

又到了一年一度的新生入学季&#xff0c;今年考上日照职业技术学院的学子们对你们的新学校有没有期待&#xff1f;下文中有途网小编给大家整理了日照职业技术学院的宿舍环境&#xff0c;供参考&#xff01;日照职业技术学院宿舍环境如何大学宿舍是各位同学们在大学期间会陪伴我…

cam350怎么看顶层_蛋糕胚速学教程大全,适合初学者看哦!

蛋糕胚速学教程大全&#xff0c;适合初学者看哦&#xff01;烘焙蛋糕最难的还是装饰&#xff0c;对手残星人来说&#xff0c;好不容易切好了蛋糕胚&#xff0c;抹面费了老大劲还是凹凸不平&#xff0c;做出来像狗狗啃过似的&#xff5e;快来看这份蛋糕抹面技巧&#xff0c;教你…

计算机一级上机考试试题题库,2016年计算机一级上机考试题库

2016年计算机一级上机考试题库第1题、 ****** 本套题共有5小题 ******(1)新建文档WDA01.DOC&#xff0c;插入文档WTA01.DOC的内容&#xff0c;将文中所有“星星”替换为“行星”&#xff0c;存储为文档WDA01.DOC。(2)新建文档WDA02.DOC&#xff0c;插入文档WDA01.DOC的内容&…

stc单片机485发送多出一字节_单片机干货!STC8H案例制作分享(内含高清实物动图)...

本期&#xff0c;Lucy制作了九个案例分享给大家&#xff0c;分别为&#xff1a;流水灯、按键LED、数码管、点阵、定时蜂鸣器、NTC温度计、超声波测距仪、光敏RGB灯、氛围灯(红外)Lucy无偿提供全部案例的原理图和部分案例的代码。有需要的朋友先关注并私信我。需要源码私信我&am…

怎么调用获取被创建的预制体_Go 语言 Web 编程系列—— 获取用户请求数据(上)...

0、GET/POST 请求数据在 PHP 中&#xff0c;可以直接通过全局变量 $_GET 和 $_POST 快速获取 GET/POST 请求数据&#xff0c;GET 请求数据主要是 URL 查询字符串中包含的参数&#xff0c;以前面在线论坛项目的群组详情页为例&#xff1a;http://localhost:8080/thread/read?id…

删除表报正在使用_U盘拔出要不要点quot;安全删除USB硬件quot;退出?

小U盘&#xff0c;大用处。U盘不仅可以用来存储各种各样的文件&#xff0c;甚至还可以用来制作电脑启动盘、Win to Go系统盘等。直接拔还是点“安全删除”后再拔U盘呢&#xff1f;在用完U盘后&#xff0c;有的会点击电脑右下角“安全删除”才拔&#xff0c;有的则会不管那么多直…

职业规划测试软件,生涯规划常用测试工具

原标题&#xff1a;生涯规划常用测试工具认识自己测试生涯规划关于生涯规划的几个测试上周我们说到中学生很有必要进行生涯规划&#xff0c;在进行生涯规划前&#xff0c;我们需要对自己有一个更清晰、更全面的认识&#xff0c;随着心理学的发展&#xff0c;很多认识自我的测试…

使用SoapUI调用不同的安全WCF SOAP服务-基本身份验证,第一部分

在这个分为三部分的系列中&#xff0c;我将演示如何使用SoapUI API工具来调用安全的WCF SOAP服务。 第一篇文章将着重于创建将要测试的系统的服务。 第二篇文章将介绍在基本身份验证机制保护的情况下调用它所需的步骤。 在最后一部分中&#xff0c;我将对初始服务稍作更改&…

配置中文_星球大战:战机中队配置需求公布 支持中文

近日《星球大战》系列新作《星球大战&#xff1a;战机中队》公布&#xff0c;该作采用寒霜引擎打造&#xff0c;支持中文。游戏将于2020年10月3日发售&#xff0c;预购价格为238元&#xff0c;登陆Xbox One/PS4/PC(Steam/Origin/Epic)平台&#xff0c;有单人和多人模式&#xf…

为什么在生产中进行硒自动化测试对于您的下一个版本至关重要?

您是否认为仅仅是因为您的Web应用程序在过渡环境中以鲜艳的色彩通过了&#xff0c;您的生产环境也将是相同的吗&#xff1f; 您可能需要重新考虑&#xff01; 特别是&#xff0c;如果我们指的是跨浏览器测试 &#xff0c;则需要确保跨各种操作系统&#xff0c;运行在不同操作系…

手机usb共享计算机网络连接,如何将手机wifi网络通过USB共享给电脑?小编教你共享方法...

曾经就有过这样的情况&#xff0c;家里突然断网了&#xff0c;这时又需要打开电脑接收文件&#xff0c;或是需要在线编辑公众号的文章&#xff0c;着急得很&#xff0c;这时电脑没有网络怎么办呢&#xff1f;能不能使用手机的流量&#xff0c;来让电脑连网呢&#xff1f;有时候…

为全局变量赋值_实例分析如何远离漫天飞舞的全局变量

前篇《由static来谈谈模块封装》基本实现了对外隐藏属性&#xff0c;隐藏局部模块函数&#xff0c;开放接口的功能。对于这个话题还有些点没有深入探讨&#xff1a;为什么要这样做&#xff1f;以及这样做的好处。或许很多刚刚开始用C或者其他面向对象编程语言(比如C)的小伙伴们…

计算机指令取决,不同的计算机,其指令不同,这主要取决于什么?

不同的计算机&#xff0c;其指令系统也不同&#xff0c;这主要取决于所用的CPU。1、CPU指中央处理器&#xff0c;是一块超大规模的集成电路&#xff0c;是一台计算机的运算核心(Core)和控制核心( Control Unit)。它的功能主要是解释计算机指令以及处理计算机软件中的数据。2、程…

栈空间_Linux中的进程栈和线程栈

1. 进程栈进程栈是属于用户态栈&#xff0c;和进程虚拟地址空间 (Virtual Address Space) 密切相关。那我们先了解下什么是虚拟地址空间&#xff1a;在 32 位机器下&#xff0c;虚拟地址空间大小为 4G。这些虚拟地址通过页表 (Page Table) 映射到物理内存&#xff0c;页表由操作…

csgo显示服务器失败,csgo服务器失败

csgo服务器失败 内容精选换一换您可以通过“应用管理”页面的应用列表&#xff0c;快速查看应用状态&#xff0c;及相关异常信息&#xff0c;如图1所示。包括&#xff1a;应用状态&#xff1a;即图1中的①应用异常信息&#xff1a;即图1中的②云服务器异常信息&#xff1a;即图…

DMN中的函数式编程:感觉就像再次重读我的大学课程一样

在本文中&#xff0c;我想分享有关DMN中的递归支持的有趣见解&#xff0c;并重点介绍FEEL语言的特定属性如何使功能编程结构能够在DMN中建模。 我们将从一个基本的示例开始&#xff0c;以演示FEEL语言和DMN构造的“商业友好”性质如何使我们能够解决一个通常不愉快的问题&…

手游极品飞车无限狂飙链接服务器失败,极品飞车无极限无法联网是什么原因 联网失败原因分析及解决方法...

有些玩家对于极品飞车无极限游戏中无法联网的问题而困扰&#xff0c;应该怎么解决呢&#xff1f;下面42824小小编就把方法分享给大家&#xff01;一、极品飞车无极限游戏无法联网原因及解决方法1、网络连接不稳定推荐在wifi的情况下进行游戏&#xff0c;如果是3G网的话很容易会…

mysql中创建唯一索引的关键字_mysql中唯一索引的关键字是什么

mysql中唯一索引的关键字是unique index。创建唯一索引可以避免数据出现重复。唯一索引可以有多个&#xff0c;但索引列的值必须唯一&#xff0c;索引列的值允许有空值。创建唯一索引可以使用关键字UNIQUE随表一同创建。mysql中唯一索引的关键字是unique index。(推荐教程&…

奇迹觉醒qq服务器比微信少,十年内最大的奇迹!功能比QQ还少的微信为什么能成功?...

今天&#xff0c;微信迎来了自己2021年的第一次「翻车」——2021年1月18日下午2点前后&#xff0c;「由于系统抖动原因」部分微信用户无法及时收取微信消息。截止下午3点19分&#xff0c;故障已被修复。其实微信曾面临过几次信息服务中断的事故&#xff1a;2013年&#xff0c;微…