从工作中清除代码–使用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,一经查实,立即删除!

相关文章

怎么使用starwind部署iscsi_2019 年总结 - 多环境多版本的部署

自己几乎经历了部署演进的所有阶段&#xff0c;手动部署、自动部署&#xff0c;部署到服务器、部署到容器。我们也在不断演进并追赶行业前沿的技术/理念。保守估计今年可以基本追赶到行业前沿的最低水平。工作中经历了部署语言的多样化&#xff0c;部署目标的演化/进化&#xf…

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

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

java 单元测试技巧_其他一些单元测试技巧

java 单元测试技巧在我以前的文章中&#xff0c;我展示了有关JavaBeans单元测试的一些技巧。 在此博客文章中&#xff0c;我将针对单元测试一些相当常见的Java代码&#xff08;即实用程序类和Log4J日志记录语句&#xff09;提供另外两个提示。 测试实用程序类 如果您的实用程序…

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

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

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

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

Java 14的新功能

2020年3月17日&#xff0c;Oracle发布了名为Java 14的Java新版本&#xff0c;其中包括许多新功能&#xff0c;工具&#xff0c;安全性&#xff0c;调试和更新的文档方面的改进。 但是&#xff0c;Oracle还向您提供Java的较旧版本&#xff0c;因为它具有向后兼容性&#xff0c;因…

计算机学校教学大纲,中等职业学校计算机应用基础教学大纲

附件4&#xff1a;中等职业学校计算机应用基础教学大纲一、课程性质与任务计算机应用基础课程是中等职业学校学生必修的一门公共基础课。本课程的任务是&#xff1a;使学生掌握必备的计算机应用基础知识和基本技能&#xff0c;培养学生应用计算机解决工作与生活中实际问题的能力…

voms下的反射大师_VOMS旧版

VOMS旧版又叫做虚拟大师&#xff0c;有独立运行的系统&#xff0c;安卓可以支持各种版本&#xff0c;自定义分辨率&#xff0c;应有游戏多开&#xff0c;还有虚拟定位的功能&#xff0c;是用户的好帮手&#xff0c;需要的朋友快来下载吧。VOMS旧版介绍VMOS 是一款运行在安卓上的…

java ssh客户端_简单的Java SSH客户端

java ssh客户端可以使用jcabi-ssh在Java中通过几行代码通过SSH执行shell命令&#xff1a; String hello new Shell.Plain(new SSH("ssh.example.com", 22,"yegor", "-----BEGIN RSA PRIVATE KEY-----...") ).exec("echo Hello, world!&qu…

hbase中的row key_hbase中RowKey的设计规则

在关心到hbase中rowkey设计的时候&#xff0c;说明hbase基本的知识已经了解了。就直接上干货(如果不了解的可以参考我上面一片关于hbase的自我总结的文章&#xff0c;我觉得总结的还是很好的)。如果文章中有错误或是不规范的地方&#xff0c;欢迎随时找我哈rowkey长度原则rowke…

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

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

赞扬精心设计:基于属性的测试如何帮助我成为更好的开发人员

开发人员的测试工具箱就是其中之一&#xff0c;很少保持不变。 可以肯定的是&#xff0c;某些测试实践已被证明比其他测试更有价值&#xff0c;但是&#xff0c;我们仍在不断寻找更好&#xff0c;更快和更具表现力的方法来测试我们的代码。 基于属性的测试 是 Java社区中鲜为人…

docker项目部署 php_「Docker部署PHP+Vue项目」- 海风纷飞Blog

创建Docker映射目录—— vue_demo # Demo项目—— php_vue—— docker-compose.yaml—— nginx———— apps # 项目代码———— conf # nginx配置文件—————— nginx.conf———— log # nginx———— vhost # 虚拟机配…

在美国本科 计算机排名2015,(word)2015年美国大学专业排名汇总-以计算机专业排名为例.doc...

(word)2015年美国大学专业排名汇总-以计算机专业排名为例美国大学经常有一些国内没有的专业&#xff0c;而且由于国情不同&#xff0c;很多在国内的热门专业&#xff0c;在国外可能不是那么“吃香”&#xff0c;另外不是名校的专业就是最好的&#xff0c;可能某个普通大学的专业…

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

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

λ演算的语法和语义_λ和副作用

λ演算的语法和语义总览 Java 8添加了诸如lambda和类型推断之类的功能。 这使语言不再那么冗长和简洁&#xff0c;但是它带来了更多的副作用&#xff0c;因为您不必对自己的工作做得那么明确。 Lambda的返回类型很重要 Java 8推断闭包的类型。 一种方法是查看返回类型&#xf…

计算机系统中存储管理是,《计算机操作系统5、存储管理.doc

《计算机操作系统5、存储管理一、选择题1&#xff0e;存储器管理的主要功能是内存分配、地址映射、内存保护和( )。A&#xff0e;2&#xff0e;把逻辑地址转变为内存的物理地址的过程称作( )A&#xff0e; D&#xff0e;重定位3&#xff0e;物理地址对应的是( )。A&#xff0e;…

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

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

Java 8 –集合sort()方法–按Employe对象(Id,名称,年龄)列出自定义排序示例...

有关在Java中对自定义对象进行排序的完整指南。 Collections.sort&#xff08;&#xff09;方法基于Comparable或Comparator实现进行排序。 用于对Employee对象进行排序的示例自定义排序 1.简介 在本教程中&#xff0c;您将学习如何在java中对Custom对象进行排序 。 首先&…

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

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