Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

воскресенье, 13 марта 2011 г.

Junit хитрости: Пишем EasyMockRunner #2

В прошлом посте "Junit хитрости: Пишем EasyMockRunner" я сформулировал задачу, а тут предлагаю ее решение.

Напомню, что тест написанный с использованием EasyMock приблизительно выглядит так
package test.com.somepackage;

import static org.junit.Assert.*;
import org.easymock.EasyMock;
import org.junit.Before;
import org.junit.Test;
import com.easymockrunner.Injector;
import com.somepackage.Dependency;
import com.somepackage.SomeObject;

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

    @Before
    public void setUp() throws Exception {
        someObject = new SomeObject();
        dependency = EasyMock.createMock(Dependency.class);
        Injector.injectAll(someObject, dependency);
    }

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

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

Много писанины и дублирования, а потому хотелось бы как-то так

package test.com.somepackage;
import static org.junit.Assert.assertEquals;

import org.easymock.EasyMock;
import org.junit.Test;
import org.junit.runner.RunWith;

import com.easymockrunner.CurrentMocks;
import com.easymockrunner.EasyMockRunner;
import com.easymockrunner.Mock;
import com.easymockrunner.TestObject;
import com.somepackage.Dependency;
import com.somepackage.SomeObject;

@RunWith(EasyMockRunner.class)
public class SomeObjectMockTest {

    @Mock
    private Dependency dependency;
    
    @TestObject
    private SomeObject someObject;
    
    @Test
    public void testThatItMocksDependency() {
        EasyMock.expect(dependency.callFromSomeObject()).andReturn("Hello");

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

Согласитесь, красивее. Ну тогда вперед!

Для начала две аннотации, которыми мы будем помечать моки и тестируемый класс
package com.easymockrunner;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestObject {

}

package com.easymockrunner;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Mock {

}

Наш старый добрый Injector, о котором я впервые упоминал в посте "Spring IoC для бедных". Класс слегка модифицирован.

package com.easymockrunner;
import java.lang.reflect.Field;

public class Injector {

    public static <T> T injectAll(T object, Object injectible) throws Exception {
        final Field[] declaredFields = object.getClass().getDeclaredFields();
        final Class<?> injectibleClass = injectible.getClass();

        boolean injected = false;

        for (Field field : declaredFields) {
            if (Object.class.equals(field.getType())) {
                continue;
            }

            if (!field.getType().isAssignableFrom(injectibleClass)) {
                continue;
            }

            injectToPrivate(object, field, injectible);

            injected = true;
        }

        if (!injected) {
            throw new IllegalArgumentException("Field not found.");
        }

        return object;
    }

    public static <T> T injectToPrivate(T object, Field field, Object injectible) throws IllegalAccessException {
        field.setAccessible(true);

        field.set(object, injectible);

        field.setAccessible(false);
        
        return object;
    }
}

Хранилище рабочих моков

package com.easymockrunner;
import java.util.Collection;
import java.util.LinkedList;

import org.easymock.EasyMock;

public class CurrentMocks {
    
    static Collection<Object> mocks = new LinkedList<Object>();
    
    public static void replay() {
        EasyMock.replay(mocks.toArray());        
    }
    
    public static void verify() {
        EasyMock.verify(mocks.toArray());        
    }
    
    public static void add(Object mock) {
        mocks.add(mock);        
    }
    
    public static void clear() {
        mocks.clear();        
    }

}

Ну и самое главное - сам раннер

package com.easymockrunner;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;

import org.easymock.EasyMock;
import org.junit.internal.runners.statements.RunAfters;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;


public class EasyMockRunner extends BlockJUnit4ClassRunner {

    private Collection<Field> mocks = new LinkedList<Field>();
    private Collection<Field> testObjects = new LinkedList<Field>();

    public EasyMockRunner(Class<?> clazz) throws InitializationError {
        super(clazz);

        for (Field field : clazz.getDeclaredFields()) {
            if (isMock(field)) {
                mocks.add(field);
            } else if (isTestObject(field)) {
                testObjects.add(field);
            }
        }
    }

    private boolean isTestObject(Field field) {
        return field.isAnnotationPresent(TestObject.class);
    }

    private boolean isMock(Field field) {
        return field.isAnnotationPresent(Mock.class);
    }
    
    public void verifyAfterTest() {
        CurrentMocks.verify();
    }
    
    @Override
    protected Statement withAfters(FrameworkMethod method, Object target, Statement statement) {
        Statement result = super.withAfters(method, target, statement);
        
        FrameworkMethod methodToRun = new FrameworkMethod(getMyMethod("verifyAfterTest"));
        return new RunAfters(result, Arrays.asList(methodToRun), this);
    }

    private Method getMyMethod(String methodName) {
        try {
            return this.getClass().getDeclaredMethod(methodName);
        } catch (SecurityException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }          
    
    @Override
    protected Object createTest() throws Exception {
        CurrentMocks.clear();
        Object test = super.createTest();

        for (Field mockField : mocks) {
            Object mockObject = EasyMock.createMock(mockField.getType());
            CurrentMocks.add(mockObject);
            Injector.injectToPrivate(test, mockField, mockObject);
        }
        
        for (Field testObjectField : testObjects) {
            Object testObject = testObjectField.getType().newInstance();

            for (Object mock : CurrentMocks.mocks) {                
                Injector.injectAll(testObject, mock);
            }

            Injector.injectToPrivate(test, testObjectField, testObject);
        }

        return test;
    }
}

Мне не нравится статика в CurrentMocks и я ее уже успешно удалил (как именно - покажу позже). Так же интересной задачкой будет избавиться от вызова CurrentMocks.replay() в каждом тесте.

Продолжение следует...

Комментариев нет:

Отправить комментарий