среда, 21 сентября 2011 г.

EasyMock + JUnit 4 с использованием аннотаций: взгляд изнутри

Этот пост является продолжением http://mirasrael.blogspot.com/2011/02/easymock-junit-4.html, в котором я показал практическое применение симбиоза аннотаций с тестовыми фреймворками.

Сейчас же, я, как и обещал, хочу рассказать о том, что стоит за сценой.

Итак, главным помощником в нашем нелегком деле является BlockJUnit4ClassRunner, являющийся ответственным за выполнение тестов в классе по умолчанию. От него мы и унаследуемся.


public class EasyMockRunner extends BlockJUnit4ClassRunner {
public EasyMockRunner(Class<?> klass) throws InitializationError {
super(klass);
...
}
...
}

Теперь мы сможем использовать этот класс, указав его в качестве раннера для нашего тестового класса.


@RunWith(EasyMockRunner.class)
public class SomeObjectTest {
...
}

Счастье было бы неполным, если бы мы не смогли добавить логику перед вызовом и после завершения каждого тестового метода. Видимо разработчики JUnit посчитали, что несчастный разработчик - плохой разработчик, и позаботились о такой возможности. Представляю Вашему вниманию methodInvoker


@Override
protected Statement methodInvoker(final FrameworkMethod method, final Object test) {
return super.methodInvoker(method, test);
}


В качестве аргументов он принимает экземпляр тестового класса и информацию о выполняемом методе, на основе которых выдает Statement (нечто очень похожее на Runnable), который будет выполнен вызовом метода statement.evaluate(). Отсюда закономерно возникает точка расширения - оборачиваем родительский стейтмент в свой и добавляем требуемую логику.


Теперь мы имеем достаточно, для того, чтобы реализовать функциональность, описанную в предыдущем посте.

1. Собираем все поля помеченные аннотацией @Mock


public EasyMockRunner(Class<?> klass) throws InitializationError {
super(klass);
collectMockedFields();
}

private void collectMockedFields() throws InitializationError {
mockedFields = new HashMap<PropertyDescriptor, FrameworkField>();
List fields = getTestClass().getAnnotatedFields(Mock.class);
for (FrameworkField field : fields) {
Mock mock = field.getField().getAnnotation(Mock.class);
field.getField().setAccessible(true);
PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(getTestObjectField().getType());
PropertyDescriptor targetProperty = null;
for (PropertyDescriptor descriptor : descriptors) {
if (descriptor.getPropertyType().isAssignableFrom(field.getField().getType()) && (mock.property().isEmpty() || descriptor.getName().equals(mock.property()))) {
if (targetProperty != null) {
throw new InitializationError("Target object have more than one property with type");
} else {
targetProperty = descriptor;
}
}
}
if (targetProperty == null) {
throw new InitializationError(String.format("Test object doesn't have property with type: %s", field.getField().getType().getName()));
} else {
mockedFields.put(targetProperty, field);
}
}
}


2. Находим поле для тестового объекта (если есть поле помеченное аннотацией @TestObject, то используем его, в противном случае пытаемся найти поле по типу ClazzTest


private Field getTestObjectField() throws InitializationError {
if (testObjectField == null) {
List<FrameworkField> fields = getTestClass().getAnnotatedFields(TestObject.class);
if (fields.size() > 1) {
throw new InitializationError("No unique test object was found");
}
if (fields.size() == 0) {
Class testClass = getTestClass().getJavaClass();
String testClassName = testClass.getName();
Class testObjectClass;
try {
testObjectClass = Class.forName(testClassName.replaceFirst("Test$", ""));
} catch (ClassNotFoundException e) {
throw new InitializationError("Test object class not found. You can set it explicitly using @Describes(clazz = TestObject.class) annotation");
}
Field targetField = null;
for (Field field : getTestClass().getJavaClass().getDeclaredFields()) {
if (testObjectClass.isAssignableFrom(field.getType())) {
if (targetField != null) {
throw new InitializationError("No unique test object was found");
}
targetField = field;
}
}
if (targetField == null) {
throw new InitializationError("Test object field was not found");
}
testObjectField = targetField;
} else {
testObjectField = fields.get(0).getField();
}
testObjectField.setAccessible(true);
}
return testObjectField;
}


3. Создаем тестовый объект


private Object createTestObject(FrameworkMethod method, List mocks) throws InitializationError, InstantiationException, IllegalAccessException {
Object testObject;Field testObjectField = getTestObjectField();
Class testObjectClass = testObjectField.getType();
MockMethodNames mockMethodNames = method.getAnnotation(MockMethodNames.class);
MockMethods mockMethodsAnnotation = method.getAnnotation(MockMethods.class);
MockMethod mockMethodAnnotation = method.getAnnotation(MockMethod.class);
List mockMethods = new ArrayList();
if (mockMethodsAnnotation != null) {
mockMethods.addAll(Arrays.asList(mockMethodsAnnotation.value()));
}
if (mockMethodAnnotation != null) {
mockMethods.add(mockMethodAnnotation);
}

if (mockMethodNames != null || !mockMethods.isEmpty()) {
IMockBuilder builder = EasyMock.createMockBuilder(testObjectClass);
if (mockMethodNames != null) {
builder.addMockedMethods(mockMethodNames.value());
}
if (!mockMethods.isEmpty()) {
Method[] methods = new Method[mockMethods.size()];
int i = 0;
for (MockMethod mockMethod : mockMethods) {
Method mockedMethod = MethodUtils.getAccessibleMethod(testObjectClass, mockMethod.name(), mockMethod.parameters());
if (mockedMethod == null) {
throw new RuntimeException(String.format("Method to mock was not found: %s", mockMethod.name()));
}
methods[i++] = mockedMethod;
}
builder.addMockedMethods(methods);
}
testObject = builder.createMock();
mocks.add(testObject);
} else {
testObject = testObjectClass.newInstance();
}
return testObject;
}


4. Оборачиваем вызов теста


@Override
protected Statement methodInvoker(final FrameworkMethod method, final Object test) {
try {
final List mocks = new ArrayList();
Object testObject = createTestObject(method, mocks);
getTestObjectField().set(test, testObject);
for (Map.Entry<PropertyDescriptor, FrameworkField> entry : this.mockedFields.entrySet()) {
Object value = EasyMock.createMock(entry.getValue().getField().getType());
mocks.add(value);
entry.getValue().getField().set(test, value);
entry.getKey().getWriteMethod().invoke(testObject, value);
}
final Statement statement = super.methodInvoker(method, test);
return new Statement() {
@Override
public void evaluate() throws Throwable {
CurrentMocks.remember(mocks);
try {
statement.evaluate();
CurrentMocks.verify();
} finally {
CurrentMocks.forgetAll();
}
}
};
} catch (Exception e) {
throw new RuntimeException(e);
}
}


5. Добавляем вспомогательный класс для связи со внешним миром


public class CurrentMocks {
private static ThreadLocal<List> currentMocks = new ThreadLocal<List>();

/**
* Remember mocks for future replay and verify
* @param mocks mocks
*/
public static void remember(Object... mocks) {
remember(Arrays.asList(mocks));
}

/**
* Remember mocks for future replay and verify
* @param mocks mocks
*/
public static void remember(List mocks) {
List allMocks = currentMocks.get();
if (allMocks != null) {
allMocks.addAll(mocks);
} else {
currentMocks.set(new ArrayList(mocks));
}
}

/**
* Shortcut for sequence:
*

* remember(mocks);
* replay();
*

* @see ru.km.easymock.CurrentMocks#remember(Object...)
* @see ru.km.easymock.CurrentMocks#replay()
* @see ru.km.easymock.CurrentMocks#verify()
* @param mocks mocks to replay and remember
*/
public static void replay(Object... mocks) {
remember(mocks);
replay();
}

/**
* Replay all remembered mocks
*/
public static void replay() {
List mocks = currentMocks.get();
if (mocks != null) {
EasyMock.replay(currentMocks.get().toArray());
}
}

/**
* Verify all remembered mocks and forget them
*/
public static void verify() {
List mocks = currentMocks.get();
if (mocks != null) {
EasyMock.verify(currentMocks.get().toArray());
forgetAll();
}
}

/**
* Forget all mocks
*/
public static void forgetAll() {
currentMocks.set(null);
}
}


6. Добавляем классы для аннотаций, которые я здесь приводить не буду, в силу их предельной простоты


вторник, 8 февраля 2011 г.

Введение в EasyMock

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


Жизненный цикл


Для начала давайте ознакомимся с тем какие стадии проходит "замоченный" компонент.


  • Создание mock компонента - на этой стадии для заданного интерфейса или класса создается прокси объект, который будет перехватывать все вызовы.

  • Запись сценария - после создания, mock переходит в record state. Это значит, что все вызовы, обращенные к нему, будут запомнены. Кроме того на этом шаге можно (и нужно) задать ожидаемое поведение для вызванного метода.

  • Воспроизведение сценария - с помощью helper метода replay, mocked объект переводится в состояние воспроизведения. Это значит, что все вызовы обращенные к нему будут сравнены с теми, которые были записаны, а так же будет воспроизведено записанное поведение (такое как возвращаемое значение).

  • Верификация - с помощью helper метода verify производится проверка того насколько полно был выполнен сценарий. Если какие-то из записанных методов не были вызваны, то это приведет к вызову исключительной ситуации.


  • Тестовый стенд


    Рассмотрим использование mock компонентов, на примере сервиса отправки почты. Допустим, у нас есть ContactService, который содержит ссылку на MailService и использует его для отправки почты.

    MailService.java

    public interface MailService {
       public boolean send(String subject, String body, String to);
    }
    

    MailServiceImpl.java

    public class MailServiceImpl { 
      //...
      public boolean send(String subject, String body, String to)  {
        //...
      }
    
      //...
    }

    ContactService.java

    public class ContactService {
      private MailService mailService;
      private String email;
    
      public void setEmail(String email) {
        this.email = email;
      }
    
      public String getEmail() {
        return email;
      }
    
      public void setMailService(MailService mailService) {
        this.mailService = mailService;
      }
    
      public void sendEmail(String subject, String body) {
        mailService.send(subject, body, email);
      }
    }


    Создание mock компонента

    Самый простой способ - это использовать статический метод EasyMock#createMock.

    MailService mailService = EasyMock.createMock(MailService.class); // теперь mailService proxy объект
    ContactServiceImpl contactService = new ContactServiceImpl(); // создаем как обычно
    contactService.setMailService(mailService); // выставляем proxy объект, в качестве e-mail сервиса
    Таким образом, мы создали mocked объект и связали его с тестируемым объектом. Но, пока что, использовать мы его не можем. Вызов любого метода будет возвращать null в качестве результата.
    Возможности EasyMock этим не ограничиваются. Например, с его помощью можно создавать partial-mocks (только часть методов перехватывается, а остальные работают как обычно), задавать параметры конструктора и какой конструктор использовать, задавать правила выполнения сценария (в той же последовательности, что и записан; в произвольной последовательности и т.п.)

    Запись сценария:

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

    mailService.send("Hello", "Hello, world", "user@mail.com"); // записываем вызов метода с перечисленными параметрами
    expectLastCall().andReturn(true); // записываем, что мы ожидаем вызов предыдущего метода и в качестве результата хотим вернуть true
    Этот же сценарий в более короткой форме можно записать так:

    expect(mailService.send("Hello", "Hello, world", "user@mail.com")).andReturn(true);
    Опять же, это лишь вершина айсберга. EasyMock позволяет использовать условия для параметров, помимо четкого соответствия (регулярное выражение, больше/меньше и т.д.); "захватывать" значения переданных параметров для дальнейшей проверки или обработки; вызывать произвольный блок кода, в ответ на вызов функции по сценарию (например, для вычисления возвращаемого значения на основе ранее "захваченного" параметра); задавать другие параметры поведения, такие как, количество вызовов метода; создавать stub методы (методы, которые всегда возвращают одно и то же значение, независимо от значений параметров и количества вызовов).

    Воспроизведение сценария:

    Теперь мы можем перейти в режим воспроизведения и вызвать метод contactService, который, в свою очередь, обратится к mocked MailService объекту

    EasyMock.replay(mailService); // сообщаем, что прокси объект mailService, нужно перевести в режим воспроизведения
    contactService.setEmail("user@mail.com");
    contactService.sendEmail("Hello", "Hello, world"); // внутри метода будет вызван mailService.send("Hello", "Hello, world", "user@mail.com"), который вернет true, как и было записано в нашем сценарии
    Если какие-либо параметры не совпадут, то вызов mailService#send приведет к ошибке. То же самое произойдет, если метод будет вызван повторно, если это не было разрешено в сценарии, либо вообще не присутствует в сценарии, либо был вызван не в том порядке. Поведение может меняться, в зависимости от механизма, который был использован для создания mock объекта.


    Верификация

    Этот шаг используется для того, чтобы убедиться, что все методы сценария были выполнены и осуществляется простым вызовом статического метода EasyMock#verify

    EasyMock.verify(mailService); // проверить правильность воспроизведения сценария


    Итог:

    В итоге выполнения всех этих шагов мы получим следующий тест:

    public void ContactServiceTest {
      @Test
      public void testThatItSendsEmail() throws Exception {
        MailService mailService = EasyMock.createMock(MailService.class);
        ContactServiceImpl contactService = new ContactServiceImpl();
        contactService.setMailService(mailService);
        expect(mailService.send("Hello", "Hello, world", "user@mail.com")).andReturn(true);
        EasyMock.replay(mailService);
        contactService.setEmail("user@mail.com");
        contactService.sendEmail("Hello", "Hello, world");
        EasyMock.verify(mailService);
      }
    }
    Примерно так выглядит простой тест, написанный с использованием EasyMock. Более подробную информацию об этой библиотеке можно получить на официальном сайте: http://easymock.org/.

суббота, 5 февраля 2011 г.

EasyMock + JUnit 4 с использованием аннотаций

"Всем хорош EasyMock, но уж больно много писать приходится", - именно с такой мыслью я начал разрабатывать решение, которое позволило бы использовать все преимущества EasyMock и при этом обойтись минимумом строчек кода.




Рассмотрим, для примера, простой код JUnit 4 теста с использованием EasyMock в стандартном стиле.


Мы имеем следующую структуру классов:


SomeObject.java

public class SomeObject {
private Dependency dependency;

public void setDependency(Dependency dependency) {
this.dependency = dependency;
}

public String callToDependency() {
return dependency.callFromSomeObject();
}
}

Dependency.java

public class Dependency {
public String callFromSomeObject() {
return "I'm here";
}
}



Тест для нее выглядел бы следующим образом:

public class SomeObjectTest {
public Dependency dependency;
public SomeObject someObject;

@Before
public void setUp() {
someObject = new SomeObject();
dependency = EasyMock.createMock(Dependency.class);
someObject.setDependency(dependency);
}

@Test
public testThatItMocksDependency() {
expect(dependency.callFromSomeObject()).andReturn("Hello");

EasyMock.replay(dependency);
testThat(someObject.callToDependency(), equalTo("Hello"));
EasyMock.verify(dependency);
}
}




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

Результатом стал следующий плод инженерной мысли:



@RunWith(EasyMockRunner.class)
public class SomeObjectTest {
@Mock public Dependency dependency;
@TestObject public SomeObject someObject;

@Test
public testThatItMocksDependency() {
expect(dependency.callFromSomeObject()).andReturn("Hello");

CurrentMocks.replay();
testThat(someObject.callToDependency(), equalTo("Hello"));
}
}


Теперь как собственно это работает:


  • EasyMockRunner собирает все поля теста, которые содержат аннотацию @Mock

  • EasyMockRunner находит тестовый объект помеченный аннотацией @TestObject

  • EasyMockRunner сопоставляет property setter's для @TestObject с @Mock полями (по типу, либо для аннотации @Mock можно напрямую указать название свойства)

  • Перед вызовом тестового метода, создается тестовый объект с использованием конструктора по умолчанию (так же может быть использована аннотация @MockedMethods для тестового метода, в этом случае @TestObject будет создан как partial mock)

  • После этого, для каждого поля с аннотацией @Mock создается mocked object и выставляется для тестового объекта и для самого теста

  • В служебный класс CurrentMocks сохраняется коллекция mocked objects, для того, чтобы можно было вызвать replay, после того как будет записан сценарий для mocked objects (сохраняется в ThreadLocal переменную)

  • В самом тесте, после записи сценария, вызывается CurrentMocks.replay()

  • В EasyMockRunner, после окончания выполнения метода, вызывается EasyMock.verify для всех mocked objects



В следующей серии я подробней расскажу (и покажу!) про классы, которые реализуют этот механизм.