Разлика между Stub, Mock и Spy в Spock Framework

1. Общ преглед

В този урок ще обсъдим разликите между Mock , Stub и Spy в рамката на Spock . Ще илюстрираме какво предлага рамката във връзка с тестовете, базирани на взаимодействие.

Spock е рамка за тестване на Java и Groovy, която помага за автоматизиране на процеса на ръчно тестване на софтуерното приложение. Той представя свои собствени макети, шушулки и шпиони и се предлага с вградени възможности за тестове, които обикновено изискват допълнителни библиотеки.

Първо ще илюстрираме кога трябва да използваме мъничета. След това ще преминем през подигравки. В крайна сметка ще опишем наскоро въведения шпионин .

2. Зависимости на Maven

Преди да започнем, нека добавим нашите зависимости Maven:

 org.spockframework spock-core 1.3-RC1-groovy-2.5 test   org.codehaus.groovy groovy-all 2.4.7 test 

Имайте предвид, че ще ни е необходима версията на Spock 1.3-RC1-groovy-2.5 . Spy ще бъде представен в следващата стабилна версия на Spock Framework. В момента Spy се предлага в първия кандидат за версия 1.3.

За обобщение на основната структура на тест на Spock, вижте нашата уводна статия за тестване с Groovy и Spock.

3. Тестване въз основа на взаимодействие

Базирането на взаимодействието е техника, която ни помага да тестваме поведението на обектите - по-конкретно как те взаимодействат помежду си. За това можем да използваме фиктивни реализации, наречени макети и заглушки.

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

Подобно на повечето Java библиотеки, Spock използва JDK динамичен прокси за подигравателни интерфейси и Byte Buddy или cglib прокси за подигравателни класове. Той създава фалшиви реализации по време на изпълнение.

Java вече има много различни и зрели библиотеки за подигравателни класове и интерфейси. Въпреки че всеки от тях може да се използва в Spock , все още има една основна причина, поради която трябва да използваме Spock макети, мъничета и шпиони. Чрез въвеждането на всичко това в Spock, ние можем да използваме всички възможности на Groovy, за да направим нашите тестове по-четливи, по-лесни за писане и определено по-забавни!

4. Обаждания по метода на забиване

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

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

Нека да преминем към примерния код с бизнес логика.

4.1. Тестван код

Нека създадем клас на модел, наречен Item :

public class Item { private final String id; private final String name; // standard constructor, getters, equals }

Трябва да заменим метода equals (Object other) , за да накараме нашите твърдения да работят. Spock ще използва равенство по време на твърдения, когато използваме двойния знак за равенство (==):

new Item('1', 'name') == new Item('1', 'name')

Сега нека създадем интерфейс ItemProvider с един метод:

public interface ItemProvider { List getItems(List itemIds); }

Ще ни трябва и клас, който ще бъде тестван. Ще добавим ItemProvider като зависимост в ItemService:

public class ItemService { private final ItemProvider itemProvider; public ItemService(ItemProvider itemProvider) { this.itemProvider = itemProvider; } List getAllItemsSortedByName(List itemIds) { List items = itemProvider.getItems(itemIds); return items.stream() .sorted(Comparator.comparing(Item::getName)) .collect(Collectors.toList()); } }

Искаме нашият код да зависи от абстракция, а не от конкретна реализация. Ето защо използваме интерфейс. Това може да има много различни реализации. Например можем да четем елементи от файл, да създаваме HTTP клиент към външна услуга или да четем данните от база данни.

В този код, ще се наложи да скъсената външната зависимост, защото ние само искаме да тестваме нашата логика, съдържаща се в getAllItemsSortedByName метод .

4.2. Използване на забит обект в тествания код

Нека инициализираме обекта ItemService в метода setup () , използвайки Stub за зависимостта ItemProvider :

ItemProvider itemProvider ItemService itemService def setup() { itemProvider = Stub(ItemProvider) itemService = new ItemService(itemProvider) }

Сега нека направим itemProvider да връща списък с елементи при всяко извикване с конкретния аргумент :

itemProvider.getItems(['offer-id', 'offer-id-2']) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]

Използваме >> операнд, за да заглушим метода. Методът getItems винаги връща списък от два елемента, когато се извиква със списък ['offer-id', 'offer-id-2'] . [] е пряк път на Groovy за създаване на списъци.

Ето целия метод за изпитване:

def 'should return items sorted by name'() { given: def ids = ['offer-id', 'offer-id-2'] itemProvider.getItems(ids) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')] when: List items = itemService.getAllItemsSortedByName(ids) then: items.collect { it.name } == ['Aname', 'Zname'] }

Има много повече възможности за зашеметяване, които можем да използваме, като например: използване на ограничения за съвпадение на аргументи, използване на последователности от стойности в заглушители, дефиниране на различно поведение при определени условия и отговори на метода на вериги.

5. Подигравателни методи на класа

Сега, нека поговорим за подигравателни класове или интерфейси в Spock.

Понякога бихме искали да знаем дали някой метод на зависимия обект е бил извикан с посочени аргументи . Искаме да се съсредоточим върху поведението на обектите и да проучим как те си взаимодействат, като разгледаме повикванията на метода.Подигравката е описание на задължителното взаимодействие между обектите в тестовия клас.

Ще тестваме взаимодействията в примерния код, който описахме по-долу.

5.1. Код с взаимодействие

За прост пример ще запишем елементи в базата данни. След успех искаме да публикуваме събитие в посредника за съобщения за нови елементи в нашата система.

Примерният брокер на съобщения е RabbitMQ или Kafka , така че обикновено ще опишем нашия договор:

public interface EventPublisher { void publish(String addedOfferId); }

Нашият метод за тестване ще запази непразни елементи в базата данни и след това ще публикува събитието. Запазването на елемент в базата данни е без значение в нашия пример, така че просто ще добавим коментар:

void saveItems(List itemIds) { List notEmptyOfferIds = itemIds.stream() .filter(itemId -> !itemId.isEmpty()) .collect(Collectors.toList()); // save in database notEmptyOfferIds.forEach(eventPublisher::publish); }

5.2. Проверка на взаимодействието с подигравани обекти

Сега, нека тестваме взаимодействието в нашия код.

Първо, трябва да се подиграем на EventPublisher в нашия метод setup () . Така че по същество ние създаваме ново поле на екземпляр и се подиграваме с него чрез функцията Mock (Class) :

class ItemServiceTest extends Specification { ItemProvider itemProvider ItemService itemService EventPublisher eventPublisher def setup() { itemProvider = Stub(ItemProvider) eventPublisher = Mock(EventPublisher) itemService = new ItemService(itemProvider, eventPublisher) }

Сега можем да напишем нашия метод за изпитване. Ще предадем 3 низа: ”, 'a', 'b' и очакваме, че нашият eventPublisher ще публикува 2 събития с 'a' и 'b' низове:

def 'should publish events about new non-empty saved offers'() { given: def offerIds = ['', 'a', 'b'] when: itemService.saveItems(offerIds) then: 1 * eventPublisher.publish('a') 1 * eventPublisher.publish('b') }

Нека разгледаме по-отблизо нашето твърдение във финалната тогава секция:

1 * eventPublisher.publish('a')

Очакваме, че itemService ще извика eventPublisher.publish (String) с 'a' като аргумент.

По-нататък говорихме за ограниченията на аргументите. Същите правила важат за подигравките. Можем да проверим, че eventPublisher.publish (String) е бил извикан два пъти с всеки ненулев и непразен аргумент:

2 * eventPublisher.publish({ it != null && !it.isEmpty() })

5.3. Комбиниране на подигравка и сбиване

In Spock, a Mock may behave the same as a Stub. So we can say to mocked objects that, for a given method call, it should return the given data.

Let's override an ItemProvider with Mock(Class) and create a new ItemService:

given: itemProvider = Mock(ItemProvider) itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')] itemService = new ItemService(itemProvider, eventPublisher) when: def items = itemService.getAllItemsSortedByName(['item-id']) then: items == [new Item('item-id', 'name')] 

We can rewrite the stubbing from the given section:

1 * itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]

So generally, this line says: itemProvider.getItems will be called once with [‘item-‘id'] argument and return given array.

We already know that mocks can behave the same as stubs. All of the rules regarding argument constraints, returning multiple values, and side-effects also apply to Mock.

6. Spying Classes in Spock

Spies provide the ability to wrap an existing object. This means we can listen in on the conversation between the caller and the real object but retain the original object behavior. Basically, Spy delegates method calls to the original object.

In contrast to Mock and Stub, we can't create a Spy on an interface. It wraps an actual object, so additionally, we will need to pass arguments for the constructor. Otherwise, the type's default constructor will be invoked.

6.1. Code Under Test

Let's create a simple implementation for EventPublisher. LoggingEventPublisher will print in the console the id of every added item. Here's the interface method implementation:

@Override public void publish(String addedOfferId) { System.out.println("I've published: " + addedOfferId); }

6.2. Testing with Spy

We create spies similarly to mocks and stubs, by using the Spy(Class) method. LoggingEventPublisher does not have any other class dependencies, so we don't have to pass constructor args:

eventPublisher = Spy(LoggingEventPublisher)

Now, let's test our spy. We need a new instance of ItemService with our spied object:

given: eventPublisher = Spy(LoggingEventPublisher) itemService = new ItemService(itemProvider, eventPublisher) when: itemService.saveItems(['item-id']) then: 1 * eventPublisher.publish('item-id')

We verified that the eventPublisher.publish method was called only once. Additionally, the method call was passed to the real object, so we'll see the output of println in the console:

I've published: item-id

Note that when we use stub on a method of Spy, then it won't call the real object method. Generally, we should avoid using spies. If we have to do it, maybe we should rearrange the code under specification?

7. Good Unit Tests

Let's end with a quick summary of how the use of mocked objects improves our tests:

  • we create deterministic test suites
  • we won't have any side effects
  • our unit tests will be very fast
  • we can focus on the logic contained in a single Java class
  • our tests are independent of the environment

8. Conclusion

В тази статия подробно описахме шпиони, подигравки и мъничета в Groovy . Познанията по този въпрос ще направят нашите тестове по-бързи, по-надеждни и по-лесни за четене.

Изпълнението на всички наши примери може да бъде намерено в проекта Github.