Java >> Java チュートリアル >  >> Tag >> assert

クリーンなテストを書く - アサーションをドメイン固有の言語に置き換える

自動化されたテストは、何もアサートしなければ意味がありませんが、通常の JUnit アサーションの問題は、それらが間違った言語を話し、多くのアサーションを作成する必要がある場合に面倒になることです。

理解しやすく維持しやすいテストを書きたい場合は、より良いアサーションの書き方を考え出す必要があります。 .

このブログ投稿では、「標準」の JUnit アサーションの問題を特定し、これらのアサーションをドメイン固有の言語に置き換えることでこれらの問題を解決する方法について説明します。

データはそれほど重要ではない

前回のブログ投稿で、データ中心のテストによって引き起こされた 2 つの問題を特定しました。そのブログ投稿では新しいオブジェクトの作成について説明しましたが、これらの問題はアサーションにも当てはまります。

registerNewUserAccount(RegistrationForm userAccountData) を確認する単体テストのソース コードを見てみましょう。 RepositoryUserService のメソッド クラスは、一意の電子メール アドレスとソーシャル サインイン プロバイダーを使用して新しいユーザー アカウントが作成されたときに、期待どおりに機能します。

単体テストは次のようになります (関連するコードが強調表示されています)。

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
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 org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
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 = "[email protected]";
	private static final String REGISTRATION_FIRST_NAME = "John";
	private static final String REGISTRATION_LAST_NAME = "Smith";
	private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;
	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_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() 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);

		assertEquals(REGISTRATION_EMAIL_ADDRESS, createdUserAccount.getEmail());
		assertEquals(REGISTRATION_FIRST_NAME, createdUserAccount.getFirstName());
		assertEquals(REGISTRATION_LAST_NAME, createdUserAccount.getLastName());
		assertEquals(SOCIAL_SIGN_IN_PROVIDER, createdUserAccount.getSignInProvider());
		assertEquals(ROLE_REGISTERED_USER, createdUserAccount.getRole());
		assertNull(createdUserAccount.getPassword());

		verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
		verify(repository, times(1)).save(createdUserAccount);
		verifyNoMoreInteractions(repository);
		verifyZeroInteractions(passwordEncoder);
	}
}

ご覧のとおり、単体テストで見つかったアサーションにより、返された User のプロパティ値が確実に オブジェクトは正しいです。私たちの主張は次のことを保証します:

  • メールの価値 プロパティは正しいです。
  • firstName の値 プロパティは正しいです。
  • lastName の値 プロパティは正しいです。
  • signInProvider の値 正解です。
  • 役割の価値 プロパティは正しいです。
  • パスワード は null です。

もちろん、これは非常に明白ですが、これらの主張をこのように繰り返すことは重要です。これは、主張の問題を特定するのに役立つからです。 私たちの主張はデータ中心です これは次のことを意味します:

  • 読み手は、返されたオブジェクトのさまざまな状態を知っている必要があります .たとえば、この例について考えてみると、読者は メール が 、 、および signInProvider 返された RegistrationForm のプロパティ オブジェクトには null 以外の値と password の値があります プロパティが null の場合、オブジェクトがソーシャル サインイン プロバイダーを使用して作成された登録であることを意味します。
  • 作成されたオブジェクトに多くのプロパティがある場合、アサーションはテストのソース コードを散らかします。返されたオブジェクトのデータが正しいことを確認したい場合でも、返されたオブジェクトの状態を記述することがはるかに重要であることを覚えておく必要があります。 .

アサーションを改善する方法を見てみましょう。

アサーションをドメイン固有の言語に変える

お気づきかもしれませんが、開発者とドメインの専門家は、同じものに対して異なる用語を使用することがよくあります。言い換えれば、開発者はドメインの専門家と同じ言語を話すわけではありません。 これは、開発者とドメインの専門家の間で不必要な混乱と摩擦を引き起こします .

ドメイン駆動設計 (DDD) は、この問題に対する 1 つのソリューションを提供します。 Eric Evans は、「ドメイン駆動設計」というタイトルの著書でユビキタス言語という用語を紹介しました。

ウィキペディアはユビキタス言語を次のように指定しています:

ユビキタス言語は、ドメイン モデルを中心に構成された言語であり、チームのすべての活動をソフトウェアと結び付けるためにすべてのチーム メンバーによって使用されます。

「正しい」言語を話すアサーションを書きたい場合は、開発者とドメインの専門家の間のギャップを埋める必要があります。つまり、アサーションを記述するためのドメイン固有の言語を作成する必要があります。

ドメイン固有言語の実装

ドメイン固有言語を実装する前に、それを設計する必要があります。アサーション用のドメイン固有言語を設計するときは、次の規則に従う必要があります。

<オール>
  • データ中心のアプローチをやめて、User から情報を見つけた実際のユーザーについてもっと考えなければなりません オブジェクト。
  • ドメインの専門家が話す言語を使用する必要があります。
  • これら 2 つのルールに従えば、ドメイン固有言語用に次のルールを作成できます。

    • ユーザーには、名、姓、メール アドレスがあります。
    • ユーザーは登録ユーザーです。
    • ユーザーはソーシャル サイン プロバイダーを使用して登録されています。つまり、このユーザーはパスワードを持っていません。

    ドメイン固有言語のルールを指定したので、それを実装する準備が整いました。ドメイン固有言語のルールを実装するカスタム AssertJ アサーションを作成することで、これを行います。

    カスタム アサーション クラスのソース コードは次のようになります。

    import org.assertj.core.api.AbstractAssert;
    import org.assertj.core.api.Assertions;
    
    public class UserAssert extends AbstractAssert<UserAssert, User> {
    
        private UserAssert(User actual) {
            super(actual, UserAssert.class);
        }
    
        public static UserAssert assertThatUser(User actual) {
            return new UserAssert(actual);
        }
    
        public UserAssert hasEmail(String email) {
            isNotNull();
    
            Assertions.assertThat(actual.getEmail())
                    .overridingErrorMessage( "Expected email to be <%s> but was <%s>",
                            email,
                            actual.getEmail()
                    )
                    .isEqualTo(email);
    
            return this;
        }
    
        public UserAssert hasFirstName(String firstName) {
            isNotNull();
    
            Assertions.assertThat(actual.getFirstName())
                    .overridingErrorMessage("Expected first name to be <%s> but was <%s>",
                            firstName,
                            actual.getFirstName()
                    )
                    .isEqualTo(firstName);
    
            return this;
        }
    
        public UserAssert hasLastName(String lastName) {
            isNotNull();
    
            Assertions.assertThat(actual.getLastName())
                    .overridingErrorMessage( "Expected last name to be <%s> but was <%s>",
                            lastName,
                            actual.getLastName()
                    )
                    .isEqualTo(lastName);
    
            return this;
        }
    
        public UserAssert isRegisteredByUsingSignInProvider(SocialMediaService signInProvider) {
            isNotNull();
    
            Assertions.assertThat(actual.getSignInProvider())
                    .overridingErrorMessage( "Expected signInProvider to be <%s> but was <%s>",
                            signInProvider,
                            actual.getSignInProvider()
                    )
                    .isEqualTo(signInProvider);
    
            hasNoPassword();
    
            return this;
        }
    
        private void hasNoPassword() {
            isNotNull();
    
            Assertions.assertThat(actual.getPassword())
                    .overridingErrorMessage("Expected password to be <null> but was <%s>",
                            actual.getPassword()
                    )
                    .isNull();
        }
    
        public UserAssert isRegisteredUser() {
            isNotNull();
    
            Assertions.assertThat(actual.getRole())
                    .overridingErrorMessage( "Expected role to be <ROLE_USER> but was <%s>",
                            actual.getRole()
                    )
                    .isEqualTo(Role.ROLE_USER);
    
            return this;
        }
    }
    

    User へのアサーションを記述するためのドメイン固有言語を作成しました。 オブジェクト。次のステップは、新しいドメイン固有言語を使用するように単体テストを変更することです。

    JUnit アサーションをドメイン固有言語に置き換える

    ドメイン固有言語を使用するようにアサーションを書き直した後、単体テストのソース コードは次のようになります (関連する部分が強調表示されています)。

    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    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 org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertNull;
    import static org.mockito.Matchers.isA;
    import static org.mockito.Mockito.times;
    import static org.mockito.Mockito.verify;
    import static org.mockito.Mockito.verifyNoMoreInteractions;
    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 = "[email protected]";
    	private static final String REGISTRATION_FIRST_NAME = "John";
    	private static final String REGISTRATION_LAST_NAME = "Smith";
    	private static final Role ROLE_REGISTERED_USER = Role.ROLE_USER;
    	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_SocialSignInAndUniqueEmail_ShouldCreateNewUserAccountAndSetSignInProvider() 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);
    
    		verify(repository, times(1)).findByEmail(REGISTRATION_EMAIL_ADDRESS);
    		verify(repository, times(1)).save(createdUserAccount);
    		verifyNoMoreInteractions(repository);
    		verifyZeroInteractions(passwordEncoder);
    	}
    }
    

    私たちのソリューションには次の利点があります:

    • 私たちの主張は、その分野の専門家が理解できる言葉を使用しています。これは、私たちのテストが理解しやすく、常に最新の実行可能な仕様であることを意味します。
    • テストが失敗した理由を理解するために時間を無駄にする必要はありません。カスタム エラー メッセージにより、失敗した理由がわかります。
    • ユーザーの API クラスが変更されたため、アサーションを User に書き込むすべてのテスト メソッドを修正する必要はありません オブジェクト。変更する必要がある唯一のクラスは、UserAssert です。 クラス。つまり、実際のアサーション ロジックをテスト メソッドから遠ざけたことで、テストの脆弱性が緩和され、保守が容易になりました。

    このブログ投稿から学んだことを要約してみましょう。

    まとめ

    これで、アサーションをドメイン固有の言語に変換しました。このブログ投稿から次の 3 つのことがわかりました。

    • データ中心のアプローチに従うと、開発者とドメイン エキスパートの間で不必要な混乱と摩擦が生じます。
    • アサーション用のドメイン固有言語を作成すると、実際のアサーション ロジックがカスタム アサーション クラスに移動されるため、テストの脆弱性が軽減されます。
    • ドメイン固有の言語を使用してアサーションを作成する場合、テストを実行可能な仕様に変換します。この仕様は理解しやすく、ドメインの専門家の言語を話します。

    Java タグ