ナニットテスト、パラメヌタ化されたテストの詳现な怜蚎。パヌトI

良い䞀日、同僚。



パラメヌタ化されたナニットテストのビゞョン、それをどのように行うか、そしおおそらくあなたがどのようにしないかしかしやりたいかを共有するこずにしたした。



䜕を正しくテストすべきかに぀いお矎しいフレヌズを曞きたいず思いたす。テストは重芁ですが、すでに倚くの資料が私の前で蚀われ、曞かれおいるので、私の意芋では、人々がめったに䜿甚しない理解するものを芁玄しお匷調したす。に移動したす。



この蚘事の䞻な目的は、オブゞェクトを䜜成するためのコヌドでナニットテストを乱雑にするのを防ぐ方法およびすべきであるず、モックanyが十分でなく、そのような状況がたくさんある堎合にテストデヌタを宣蚀的に䜜成する方法を瀺すこずです。



mavenプロゞェクトを䜜成し、junit5、junit-jupiter-params、mokitoを远加しおみたしょう。



完党に退屈にならないように、テストからすぐに曞き始めたす。TDDの謝眪者が奜むように、宣蚀的にテストするサヌビスが必芁です。



テストHabrServiceTestを䜜成したしょう。テストクラスフィヌルドにHabrServiceぞのリンクを远加したす。



public class HabrServiceTest {

    private HabrService habrService;

    @Test
    void handleTest(){

    }
}


ideを介しおサヌビスを䜜成しショヌトカットを軜く抌すこずにより、フィヌルドに@InjectMocksアノテヌションを远加したす。



テストから盎接始めたしょう。小さなアプリケヌションのHabrServiceには、単䞀のHabrItem匕数を受け取る単䞀のhandleメ゜ッドがあり、テストは次のようになりたす。



public class HabrServiceTest {

    @InjectMocks
    private HabrService habrService;

    @Test
    void handleTest(){
        HabrItem item = new HabrItem();
        habrService.handle(item);
    }
}


HabrServiceにhandleメ゜ッドを远加しおみたしょう。これは、モデレヌトされおデヌタベヌスに保存された埌、Habréの新しい投皿のIDを返し、HabrItemタむプを取り、HabrItemも䜜成したす。これで、テストはコンパむルされたすが、クラッシュしたす。



重芁なのは、期埅される戻り倀のチェックを远加したこずです。



public class HabrServiceTest {

    @InjectMocks
    private HabrService habrService;

    @BeforeEach
    void setUp(){
        initMocks(this);
    }

    @Test
    void handleTest() {
        HabrItem item = new HabrItem();
        Long actual = habrService.handle(item);

        assertEquals(1L, actual);
    }
}


たた、handleメ゜ッドの呌び出し䞭に、ReviewServiceずPersistanceServiceが呌び出され、厳密に次々に呌び出され、1回だけ機胜し、他のメ゜ッドが呌び出されなくなったこずを確認したいず思いたす。蚀い換えれば、このように



public class HabrServiceTest {

    @InjectMocks
    private HabrService habrService;

    @BeforeEach
    void setUp(){
        initMocks(this);
    }

    @Test
    void handleTest() {
        HabrItem item = new HabrItem();
        
        Long actual = habrService.handle(item);
        
        InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
        inOrder.verify(reviewService).makeRewiew(item);
        inOrder.verify(persistenceService).makePersist(item);
        inOrder.verifyNoMoreInteractions();

        assertEquals(1L, actual);
    }
}


クラスクラスのフィヌルドにreviewServiceメ゜ッドずpersistenceServiceを远加し、それらを䜜成し、makeRewiewメ゜ッドずmakePersistメ゜ッドをそれぞれ远加したす。これですべおがコンパむルされたすが、もちろんテストは赀です。



この蚘事のコンテキストでは、ReviewServiceずPersistanceServiceの実装はそれほど重芁ではなく、HabrServiceの実装は重芁です。今よりも少し面癜くしたしょう。



public class HabrService {

    private final ReviewService reviewService;

    private final PersistenceService persistenceService;

    public HabrService(final ReviewService reviewService, final PersistenceService persistenceService) {
        this.reviewService = reviewService;
        this.persistenceService = persistenceService;
    }

    public Long handle(final HabrItem item) {
        HabrItem reviewedItem = reviewService.makeRewiew(item);
        Long persistedItemId = persistenceService.makePersist(reviewedItem);

        return persistedItemId;
    }
}


そしお、when。then構文を䜿甚しお、補助コンポヌネントの動䜜をロックしたす。その結果、テストは次のようになり、緑色になりたす。



public class HabrServiceTest {

    @Mock
    private ReviewService reviewService;

    @Mock
    private PersistenceService persistenceService;

    @InjectMocks
    private HabrService habrService;

    @BeforeEach
    void setUp() {
        initMocks(this);
    }

    @Test
    void handleTest() {
        HabrItem source = new HabrItem();
        HabrItem reviewedItem = mock(HabrItem.class);

        when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
        when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);

        Long actual = habrService.handle(source);

        InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
        inOrder.verify(reviewService).makeRewiew(source);
        inOrder.verify(persistenceService).makePersist(reviewedItem);
        inOrder.verifyNoMoreInteractions();

        assertEquals(1L, actual);
    }
}


パラメヌタ化されたテストの力を実蚌するためのモックアップが甚意されおいたす。



ハブタむプhubTypeのフィヌルドをHabrItemサヌビスのリク゚ストモデルに远加し、列挙型HubTypeを䜜成しお、いく぀かのタむプを含めたす。



public enum HubType {
    JAVA, C, PYTHON
}


HabrItemモデルの堎合、䜜成されたHubTypeフィヌルドにゲッタヌずセッタヌを远加したす。



HabrServiceの深郚にスむッチが隠されおいるずしたす。これは、ハブのタむプに応じお、リク゚ストで䞍明な凊理を実行したす。䞍明なケヌスをそれぞれテストするテストでは、メ゜ッドの単玔な実装は次のようになりたす。



        
    @Test
    void handleTest() {
        HabrItem reviewedItem = mock(HabrItem.class);
        HabrItem source = new HabrItem();
        source.setHubType(HubType.JAVA);

        when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
        when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);

        Long actual = habrService.handle(source);

        InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
        inOrder.verify(reviewService).makeRewiew(source);
        inOrder.verify(persistenceService).makePersist(reviewedItem);
        inOrder.verifyNoMoreInteractions();

        assertEquals(1L, actual);
    }


テストをパラメヌタ化し、列挙型からランダムな倀をパラメヌタずしお远加するこずで、少しきれいで䟿利にするこずができたす。その結果、テスト宣蚀は次のようになりたす。



@ParameterizedTest
    @EnumSource(HubType.class)
    void handleTest(final HubType type) 


うたく、宣蚀的に、そしお私たちの列挙のすべおの倀は間違いなく次のテストの実行で䜿甚されたす、泚釈にはパラメヌタヌがあり、包含、陀倖の戊略を远加できたす。



しかし、おそらく私は、パラメヌタ化されたテストが優れおいるずあなたに確信させおいたせん。远加

元のHabrItemリク゚ストは新しいeditCountフィヌルドであり、Habrナヌザヌが蚘事を線集する数千回が投皿前に曞き蟌たれるため、少なくずも少しは気に入っおいただけたす。HabrServiceの奥のどこかに、未知のこずを行う䜕らかのロゞックがあるず想定したす。䜜成者が詊した量に応じお、考えられるすべおのeditCountオプションに察しお5぀たたは55のテストを蚘述したくないが、宣蚀的にテストしたい堎合はどうなりたすか1぀の堎所のどこかに、確認したいすべおの倀をすぐに瀺したす..。これ以䞊簡単なものはありたせん。パラメヌタ化されたテストのapiを䜿甚するず、メ゜ッド宣蚀で次のようなものが埗られたす。



    @ParameterizedTest
    @ValueSource(ints = {0, 5, 14, 23})
    void handleTest(final int type) 


問題がありたす、宣蚀的に䞀床にテストメ゜ッドパラメヌタに2぀の倀を収集したいので、パラメヌタ化されたテストの別の優れたメ゜ッド@CsvSourceを䜿甚できたす。これは、単玔な出力倀ナヌティリティクラスのテストに非垞に䟿利で、単玔なパラメヌタのテストに最適ですが、オブゞェクトがはるかに耇雑になった堎合はどうなりたすかプリミティブずJavaタむプだけでなく、玄10個のフィヌルドがあるずしたしょう。



@MethodSourceアノテヌションが圹に立ち、テストメ゜ッドが著しく短くなり、セッタヌがなくなり、着信芁求の゜ヌスがパラメヌタヌずしおテストメ゜ッドに送られたす。



    
    @ParameterizedTest
    @MethodSource("generateSource")
    void handleTest(final HabrItem source) {
        HabrItem reviewedItem = mock(HabrItem.class);

        when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
        when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);

        Long actual = habrService.handle(source);

        InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
        inOrder.verify(reviewService).makeRewiew(source);
        inOrder.verify(persistenceService).makePersist(reviewedItem);
        inOrder.verifyNoMoreInteractions();

        assertEquals(1L, actual);
    }


@MethodSourceアノテヌションにはgenerateSource文字列がありたすが、それは䜕ですかこれは、必芁なモデルを収集するメ゜ッドの名前です。その宣蚀は次のようになりたす。



   private static Stream<Arguments> generateSource() {
        HabrItem habrItem = new HabrItem();
        habrItem.setHubType(HubType.JAVA);
        habrItem.setEditCount(999L);
        
        return nextStream(() -> habrItem);
    }


䟿宜䞊、nextStream匕数のストリヌムの圢成を別のナヌティリティテストクラスに移動したした。



public class CommonTestUtil {
    private static final Random RANDOM = new Random();

    public static <T> Stream<Arguments> nextStream(final Supplier<T> supplier) {
        return Stream.generate(() -> Arguments.of(supplier.get())).limit(nextIntBetween(1, 10));
    }

    public static int nextIntBetween(final int min, final int max) {
        return RANDOM.nextInt(max - min + 1) + min;
    }
}


これで、テストを開始するず、HabrItemリク゚ストモデルが宣蚀的にテストメ゜ッドパラメヌタに远加され、テストはテストナヌティリティによっお生成された匕数の数



この堎合は1から10で実行されたす。これは、モデルが匕数のストリヌムにある堎合に特に䟿利です。この䟋のようにハヌドコヌドではなく、ランダマむザヌを䜿甚しお収集されたす長生きするフロヌティングテストですが、そうである堎合は問題がありたす。



私の意芋では、すべおがすでにスヌパヌであり、テストは珟圚、スタブの動䜜ず期埅される結果のみを蚘述しおいたす。



しかし、これが䞍運です。新しいフィヌルド、テキスト、文字列の配列がHabrItemモデルに远加されたす。これは、非垞に倧きい堎合ずそうでない堎合がありたす。問題ではありたせん。䞻なこずは、テストを乱雑にしたくない、ランダムデヌタは必芁ない、厳密に定矩されたモデルが必芁なこずです。特定のデヌタを䜿甚しお、テストたたはその他の堎所でそれを収集したす-私たちは望んでいたせん。郵䟿配達員など、どこからでもjsonリク゚ストの本文を取埗し、それに基づいおモックファむルを䜜成し、テストで宣蚀的にモデルを䜜成しお、デヌタを含むjsonファむルぞのパスのみを指定できれば䟿利です。



優秀な。 @JsonSourceアノテヌションを䜿甚したす。これは、盞察ファむルパスずタヌゲットクラスずずもに、パスパラメヌタヌを取りたす。なんおこったパラメヌタ化されたテストにはそのような泚釈はありたせんが、私はそれが欲しいです。



自分で曞いおみたしょう。



ArgumentsProviderは、junitの@ParametrizedTestに付属するすべおの泚釈の凊理を担圓したす。独自のJsonArgumentProviderを蚘述したす。



public class JsonArgumentProvider implements ArgumentsProvider, AnnotationConsumer<JsonSource> {

    private String path;

    private MockDataProvider dataProvider;

    private Class<?> clazz;

    @Override
    public void accept(final JsonSource jsonSource) {
        this.path = jsonSource.path();
        this.dataProvider = new MockDataProvider(new ObjectMapper());
        this.clazz = jsonSource.clazz();
    }

    @Override
    public Stream<Arguments> provideArguments(final ExtensionContext context) {
        return nextSingleStream(() -> dataProvider.parseDataObject(path, clazz));
    }
}


MockDataProviderは、モックjsonファむルを解析するためのクラスであり、その実装は非垞に簡単です。




public class MockDataProvider {

    private static final String PATH_PREFIX = "json/";

    private final ObjectMapper objectMapper;

     public <T> T parseDataObject(final String name, final Class<T> clazz) {
        return objectMapper.readValue(new ClassPathResource(PATH_PREFIX + name).getInputStream(), clazz);
    }

}


モックプロバむダヌの準備が敎いたした。アノテヌションの匕数プロバむダヌでもありたす。アノテヌション自䜓を远加する必芁がありたす。




/**
 * Source-   ,
 *     json-
 */
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(JsonArgumentProvider.class)
public @interface JsonSource {

    /**
     *   json-,   classpath:/json/
     *
     * @return     
     */
    String path() default "";

    /**
     *  ,        
     *
     * @return  
     */
    Class<?> clazz();
}


やったヌ。アノテヌションを䜿甚する準備が敎いたした。テスト方法は次のずおりです。



  
    @ParameterizedTest
    @JsonSource(path = MOCK_FILE_PATH, clazz = HabrItem.class)
    void handleTest(final HabrItem source) {
        HabrItem reviewedItem = mock(HabrItem.class);

        when(reviewService.makeRewiew(source)).thenReturn(reviewedItem);
        when(persistenceService.makePersist(reviewedItem)).thenReturn(1L);

        Long actual = habrService.handle(source);

        InOrder inOrder = Mockito.inOrder(reviewService, persistenceService);
        inOrder.verify(reviewService).makeRewiew(source);
        inOrder.verify(persistenceService).makePersist(reviewedItem);
        inOrder.verifyNoMoreInteractions();

        assertEquals(1L, actual);
    }


モックjsonでは、必芁な数のオブゞェクトを非垞に迅速に䜜成できたす。これからは、テストデヌタの圢成のために、テストの本質を損なうコヌドはありたせん。もちろん、モックで実行できるこずはよくありたすが、垞にそうずは限りたせん。



芁玄するず、私は次のように蚀いたいず思いたす。私たちは、䜕幎もの間、矎しく簡単にできるこずを考えずに、䜕幎も前ず同じように仕事をしおいるこずがよくありたす。



PSこの蚘事は、TDDの抂念に関する知識を目的ずしたものではありたせん。ストヌリヌテリングキャンペヌンにテストデヌタを远加しお、もう少し明確で興味深いものにしたいず思いたした。



All Articles