Тестване на състояние спрямо тестване на взаимодействие

Обикновено има два начина, по които unit тестовете могат да проверяват дали кодът, който е тестван, работи правилно: като се тества състояние или като се тества взаимодействие. Каква е разликата между тези два подхода?

„Тестването на състояние означава, че се проверява дали тестваният код връща правилния резултат.”

@Test
public void findRegisteredUser() {
User alhponse = new User("firstId", "Alhponse");

UserRegistry registry = new PersistentUserRegistry(inMemoryStorage);
 registry.registerUser(alphonse);

User foundUser = registry.findUser("firstId");
assertEquals(alphonse, foundUser);
}

В този пример става ясно, че ние сме способни да намерим даден потребител след като сме го регистрирали. В същото време не разбираме какви са имплементационните детайли на PersistentUserRegistry, което прави теста изолиран от промени в същия клас.

„Тестване на взаимодействие означава, че се проверява дали тестваният код извиква правилно методи на други компоненти.”

@Test
public void findUser() {
User alhponse = new User("firstId", "Alhponse");
 when(mockStorage.findById("firstId")).thenReturn(alphonse);
UserRegistry registry = new PersistentUserRegistry(mockStorage);
 User foundUser = registry.findUser("firstId");
assertEquals(alphonse, foundUser);
}

С втория пример проверяваме как при търсене на потребител по конкретно ID ще бъде извикан метод за търсене със същия параметър и резултатът от взаимодействието с този метод ще бъде върнат. Важно е да споменем, че при така написан тест не винаги можем да гарантираме желаното намерение чрез конкретните взаимодействия.

Избор на подход

Правилно взаимодействие довело до грешно състояние

Желанието да пишем тестове трябва да изхожда от мотивацията ни да пишем качествен код, както и да повиши нашата увереност в изправността му. Решението как да напишем теста би зависило от това за какво искаме да добием увереност. Там където бихме искали да се уверим, че дадено поведение е имплементирано коректно, без да е важно какви са детайлите на имплементацията, бихме използвали подхода, при който тестваме състояние. От друга страна, ако целим да проверим как точно се реализира желаното поведение, бихме избрали вариант, при който подсигуряваме правилното взаимодействие с други компоненти. Така, например при колаборация с мрежата може да се застраховаме, че ще бъдат направени желания брой заявки, било то до база данни или външна система.

Martin Fowler разграничава гореспоменатите подходи като Sociable и Solitary тестове. При общителен (Sociable) тест компонентът, който тестваме, комуникира с други компоненти за да осъществи своето поведение. Докато при самотния (Solitary) тест кодът, който тестваме, е в изолация и компонентът общува с мокове.
Истината е, че няма точна формула, за това кой подход трябва да се използва. Изборът ни би зависил изцяло от ситуацията, структурата на кода и поведението, което искаме да тестваме.

Disclaimer

Unit тестовете не дефинират строга рамка относно това, до каква степен взаимодействащи помежду си компоненти трябва да бъдат изолирани по време на тестване. Разбира се, това не означава, че не трябва да бъде прилаган здрав разум и да не се добавя изолация там, където може да се получи недетерминистичен резултат.