Статьи

Почему модульное тестирование имеет значение

У меня была возможность взглянуть на модульное тестирование с трех разных точек зрения — разработчика, архитектора и директора. Мое мнение изменилось от мысли о модульном тестировании как о необходимом зле для проектировщика. 

После управления продуктом, содержащим более полумиллиона строк кода, мне была поручена реализация электронной коммерции. Я начал с цели достижения 100% охвата модульных тестов. Поддержание качества кода очень важно для больших проектов в распределенных командах. Статический анализ кода и модульное тестирование являются частью стандартного инструментария в пактиках разработки программного обеспечения.

Проект электронной коммерции реализуется с использованием ATG. ATG существует уже более 15 лет и за эти годы превратилась в зрелое решение JEE. Разработка продукта ATG началась еще до того, как TDD была в моде, и с тех пор не получила особой поддержки TDD. Было несколько попыток предоставить инструменты, необходимые для тестирования кода ATG с компонентами Nucleus. Несколько заметных упоминаний о  DUST   и  DynaCactus . Оба эти инструмента предоставили возможность вставлять компоненты Nucleus в ваш код. Они также требовали, чтобы сервер приложений был готов к использованию компонента Nucleus. 

Основное внимание в этой статье будет уделено использованию JUnit с Mockito. Mockito — это гибкая библиотека-макет, которая позволяет быстро разрабатывать тесты. С аннотациями написать модульный тест Mockito очень просто. Тесты выполняются быстро, потому что они не инициализируют всю зависимость класса 

Что я должен проверить?

  1. Проверка состояния — JUnit поставляется с удобным набором утверждений для проверки состояния объекта после выполнения программы. Традиционный тест JUnit, основанный только на проверке состояния, не гарантирует, что программа прошла путь выполнения, разработанный программистом.

  2. Проверка поведения — Иногда «тестируемая система» может не иметь состояния или система не может изменить состояние после выполнения. В этих случаях уместен тест, проверяющий поведение или взаимодействие. Проверка поведения фокусируется на том, чтобы программа следила за выполнением путь, определяемый программистом. Модулирующие среды помогают нам проверять поведение и взаимодействие SUT.

В этой статье мы увидим примеры проверки состояния и поведения. Behavior Driven Development (BDD) является расширением Test Driven Development и помогает вам идентифицировать тесты, которые проверяют поведение, требуемое бизнес-требованиями. BDD станет темой для другого обсуждения и не будет освещаться в этой статье.

Вариант использования

Мы будем использовать простой вариант использования для демонстрации использования Mockito с ATG. Владелец продукта хотел бы ограничить максимальное количество, которое человек может заказать для любого данного товара. Реализация требует, чтобы CommerceItemManager был расширен с помощью метода — addItemQuantityToShippingGroup.

public void addItemQuantityToShippingGroup(Order pOrder, String pCommerceItemId, String pShippingGroupId, long pQuantity) throws CommerceException {

  long quantity = pQuantity;
  ShippingGroupCommerceItemRelationship rel = null;
  CommerceItem item = pOrder.getCommerceItem(pCommerceItemId);
  boolean exists = true;
  try {
    rel = getShippingGroupManager().getShippingGroupCommerceItemRelationship(pOrder, pCommerceItemId, pShippingGroupId);
  } catch (RelationshipNotFoundException e) {
      exists = false;
  }
  long unassignedQty = getUnassignedQuantityForCommerceItem(item);

  if (quantity > unassignedQty) {
    quantity = unassignedQty;
  }

  if (unassignedQty == LONG_INIT_VAL) {
    unassignedQty = getShippingGroupManager().getRemainingQuantityForShippingGroup(item);
  }
  if (quantity > unassignedQty) {
    throw new InvalidParameterException(ResourceUtils.getMsgResource("QuantityTooBig", ORDER_RESR, sResourceBundle));
  }
  if (exists) {
    if (rel.getRelationshipType() != 100) {
      throw new InvalidTypeException(ResourceUtils.getMsgResource("IncompatibleRelationshipType", ORDER_RESR, sResourceBundle));
    }
    rel.setQuantity(rel.getQuantity() + quantity);
  } else {
     rel = (ShippingGroupCommerceItemRelationship) getOrderTools().createRelationship("shippingGroupCommerceItem");
     rel.setRelationshipType(100);
     getOrderTools().initializeRelationship(pOrder, rel, pShippingGroupId, pCommerceItemId);
     rel.setQuantity(quantity);
  }

}

Реализация имеет несколько условных выражений в одном методе и несколько внутренних вызовов методов. Мы можем обсудить, насколько хорошо метод структурирован, но мы начали с предпосылки, что мы пишем модульные тесты для унаследованной реализации.

Хороший тест охватывает все пути, которые ваш метод собирается пройти в процессе выполнения. Вы должны создать один или несколько модульных тестов для каждого условия в методе. 

Давайте начнем с настройки тестового примера и насмешек над объектами, которые используются в реализации. 

public class CustomCommerceItemManagerTest {

@Spy
CustomCommerceItemManager commerceItemManager;

@Mock
Order order;

@Mock
CustomOrderTools orderTools;

@Mock
CustomCommerceItemImpl commerceItemInstance;
@Mock
CustomPriceListManager pricelistManager;
@Mock
ShippingGroupManager shipGroupManager;
@Mock
ShippingGroupCommerceItemRelationship relationship;

//Set up the test
String commerceItemId = "ci456";
String shippingGroupId = "sg123";

@Before
public void setUp() throws Exception {
initMocks(this);

commerceItemManager.setOrderTools(orderTools);

commerceItemManager.setPriceListManager(pricelistManager);

commerceItemManager.setShippingGroupManager(shipGroupManager);

}

}

Мы используем аннотацию @Mock для насмешки зависимых классов. Строка 3 использует аннотацию @Spy для экземпляра CustomCommerceItemManager. 

Mockito Spy, как следует из названия, используется для шпионажа на реальном объекте. Spy пригодится, когда у нас есть объекты, управляемые контейнером IoC. Мы будем использовать шпионов Mockito для проверки поведения объекта, переопределения поведения объекта и заставить ваши объекты ничего не делать, если мы того пожелаем. Читайте больше на Шпионе Mockito  здесь .

Тестовый пример 1 

Метод пытается найти ShippingGroupCommerceItemRelationship (rel). Если он находит существующее отношение, он использует его. В противном случае это создает новые отношения.

  ShippingGroupCommerceItemRelationship rel = null;
  boolean exists = true;
  try {
    rel = getShippingGroupManager().getShippingGroupCommerceItemRelationship(pOrder, pCommerceItemId, pShippingGroupId);
  } catch (RelationshipNotFoundException e) {
      exists = false;
  }
...
...
...
if (exists) {
    if (rel.getRelationshipType() != 100) {
      throw new InvalidTypeException(ResourceUtils.getMsgResource("IncompatibleRelationshipType", ORDER_RESR, sResourceBundle));
    }
    rel.setQuantity(rel.getQuantity() + quantity);
  } else {
     rel = (ShippingGroupCommerceItemRelationship) getOrderTools().createRelationship("shippingGroupCommerceItem");
}

При написании тестовых примеров хорошей практикой является разбиение теста на три части: настройка поведения, вызов тестируемого метода и проверка поведения. Тестовый пример JUnit — это также код, хорошо продумайте его и сделайте так же красиво, как и вашу реализацию. Бонусные баллы, если вы можете добавить комментарии;). Давайте посмотрим на наш тестовый пример:

@SuppressWarnings("unchecked")
@Test
public void shouldCreateRelationshipAndAddItem() throws CommerceException {
// Mock the behavior
Mockito.when(order.getCommerceItem(Mockito.anyString())).thenReturn(
commerceItemInstance);

Mockito.when(
commerceItemManager.getShippingGroupManager()
.getShippingGroupCommerceItemRelationship(order,
commerceItemId, shippingGroupId)).thenThrow(
RelationshipNotFoundException.class);
Mockito.when(orderTools.createRelationship("shippingGroupCommerceItem"))
.thenReturn(relationship);

// Invoke the Method under test
commerceItemManager.addItemQuantityToShippingGroup(order, commerceItemId,
shippingGroupId, 2L);


//verify behavior
Mockito.verify(commerceItemManager.getOrderTools())
.createRelationship(Mockito.anyString());

Mockito.verify(commerceItemManager, Mockito.times(2))
.addItemQuantityToShippingGroup(order, commerceItemId, shippingGroupId,
2L);

//Verify State
assertEquals(relationship.getQuantity(),2L);

}

Контрольный пример 2: тестирование исключительных сценариев

JUnit4 поддерживает обработку и ожидание исключений во время выполнения метода. 

@Test(expected=InvalidTypeException.class)
public void shouldThrowInvalidTypeExceptionForIncompatibleRelationshipType() throws CommerceException {
// Mock the behavior
Mockito.when(order.getCommerceItem(Mockito.anyString())).thenReturn(
commerceItemInstance);

Mockito.when(
commerceItemManager.getShippingGroupManager()
.getShippingGroupCommerceItemRelationship(order,
commerceItemId,shippingGroupId )).thenReturn(
relationship);
Mockito.when(relationship.getRelationshipType()).thenReturn(101);
Mockito.when(relationship.getQuantity()).thenReturn(1L);
Mockito.when(commerceItemInstance.getQuantity()).thenReturn(2L);

// Invoke the Method under test
commerceItemManager.addItemQuantityToShippingGroup(order, commerceItemId,
shippingGroupId, 2L);

/* The test is expected to throw an exception of Type InvalidTypeException
and is annotated to pass the test when it encounters that exception.
*/
}

Резюме

Мы продемонстрировали использование JUnit + Mockito для тестирования приложения ATG. Написание модульных тестов добавляет стабильности и делает ваш код более понятным. Хорошо написанный модульный тест также служит документацией вашего кода. Структурируйте свои юнит-тесты так же, как и реализацию. Хорошей практикой является написание нескольких тестовых случаев для каждого тестируемого метода, охватывающих каждый путь, по которому будет идти метод. По мере увеличения охвата модульных тестов вашего приложения вы заметите, что код становится более модульным и читаемым. 

Ресурсы:

http://martinfowler.com/articles/mocksArentStubs.html