Шаблонный метод - шаблоны проектирования в деталях

Шаблонный метод - это популярный поведенческий шаблон проектирования. При его помощи можно задать некоторое поведение в методе абстрактного класса, но при этом оставить детали реализации на классы-наследники. Проще говоря, в контексте языка программирования Java шаблонный метод - это метод абстрактного класса или метод по умолчанию интерфейса, задающий общее поведение, но опирающийся на другие абстрактные методы. Шаблонный метод наряду с шаблоном проектирования "Стратегия" часто применяется при реализации инверсии управления.

Демонстрируя пример реализации шаблона проектирования "Декоратор", я использовал и шаблонный метод в классе CachingFindTaskByIdDecorator:

abstract class CachingFindTaskByIdDecorator implements FindTaskByIdSpi {

    private FindTaskByIdSpi delegate;

    abstract Optional<TaskData> retrieveFromCache(UUID id);

    abstract void storeInCache(TaskData task);

    CachingFindTaskByIdDecorator(FindTaskByIdSpi delegate) {
        this.delegate = delegate;
    }

    public Optional<TaskData> findTaskById(UUID id) {
        return this.retrieveFromCache(id)
            .or(() -> this.delegate.findTaskById(id)
                    .map(task -> {
                        this.storeInCache(task);
                        return task;
                    }));
    }
}

Метод findTaskById в данном случае реализует шаблон проектирования "Шаблонный метод", он задаёт некоторое поведение, но оставляет детали реализации, поиск и сохранение задачи в кэше, на классы-наследники.

Тестирование шаблонного метода

Как правило, тестирование шаблонного метода происходит при тестировании классов-наследников. Однако возможна ситуация, когда классов-наследников у абстрактного класса, содержащего шаблонный метод, нет наследников в рамках проекта. Происходит это в случаях, когда вы разрабатываете какой-то фреймворк или библиотеку и хотите, чтобы реализацию этого абстрактного класса предоставлял пользователь вашей библиотеки. В этом случае всё же желательно написать тест для абстрактного класса, содержащего шаблонный метод. Сделать это можно двумя способами: при помощи тестовой реализации абстрактного класса или при помощи фреймворков для создания объектов-имитаций, например Mockito.

В первом случае вам необходимо создать в директории тестовых классов максимально простой класс, реализующий тестируемый абстрактный класс. Для CachingFindTaskByIdDecorator тестовая реализация может выглядеть следующим образом:

class TestingCachingFindTaskByIdDecorator implements CachingFindTaskByIdDecorator {

    TestingCachingFindTaskByIdDecorator(FindTaskByIdSpi delegate) {
        super(delegate);
    }

    TaskData cachedTask;

    @Override
    Optional<TaskData> retrieveFromCache(UUID id) {
        return Optional.ofNullable(this.cachedTask);
    }

    @Override
    void storeInCache(TaskData task) {
        this.cachedTask = task;
    }
}

Теперь необходимо написать тест для TestingCachingFindTaskByIdDecorator:

@ExtendWith(MockitoExtension.class)
class TestingCachingFindTaskByIdDecoratorTest {

    @Mock
    FindTaskByIdSpi delegate;

    TestingCachingFindTaskByIdDecorator decorator;

    @BeforeEach
    void setUp() {
        this.decorator = spy(new TestingCachingFindTaskByIdDecorator(this.delegate));
    }

    @Test
    void findTaskById_TaskIsCached_ReturnsNotEmptyOptional() {
        // given
        var taskId = UUID.randomUUID();
        var task = new TaskData(taskId);

        this.decorator.cachedTask = task;

        // when
        var optional = this.decorator.findTaskById(taskId);

        // then
        assertEquals(Optional.of(task), optional);

        verify(this.decorator, never()).storeInCache(any());
    }

    @Test
    void findTaskById_TaskIsNotCached_ReturnsNotEmptyOptional() {
        // given
        var taskId = UUID.randomUUID();
        var task = new TaskData(taskId);

        doReturn(Optional.of(task)).when(this.delegate).findTaskById(taskId);

        // when
        var optional = this.decorator.findTaskById(taskId);

        // then
        assertEquals(Optional.of(task), optional);

        verify(this.decorator).storeInCache(task);
    }

    @Test
    void findTaskById_TaskDoesNotExist_ReturnsEmptyOptional() {
        // given
        var taskId = UUID.randomUUID();

        // when
        var optional = this.decorator.findTaskById(taskId);

        // then
        assertEquals(Optional.empty(), optional);

        verify(this.decorator, never()).storeInCache(any());
    }
}

Я не сторонник создания классов специально для тестов вроде TestingCachingFindTaskByIdDecorator, поэтому предлагаю рассмотреть второй вариант. В нём необходимо будет создать объект-имитацию CachingFindTaskByIdDecorator, но использовать реальный метод findTaskById. Кроме этого при создании объекта-имитации потребуется использовать правильный конструктор для передачи декорируемого объекта.

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

@ExtendWith(MockitoExtension.class)
class CachingFindTaskByIdDecoratorTest {

    @Mock
    FindTaskByIdSpi delegate;

    CachingFindTaskByIdDecorator decorator;

    @BeforeEach
    void setUp() {
        this.decorator = mock(CachingFindTaskByIdDecorator.class, withSettings()
                .useConstructor(this.delegate));
        doCallRealMethod().when(this.decorator).findTaskById(any());
    }

    @Test
    void findTaskById_TaskIsCached_ReturnsNotEmptyOptional() {
        // given
        var taskId = UUID.randomUUID();
        var task = new TaskData(taskId);

        doReturn(Optional.of(task)).when(this.decorator).retrieveFromCache(taskId);

        // when
        var optional = this.decorator.findTaskById(taskId);

        // then
        assertEquals(Optional.of(task), optional);

        verify(this.decorator, never()).storeInCache(any());
    }

    @Test
    void findTaskById_TaskIsNotCached_ReturnsNotEmptyOptional() {
        // given
        var taskId = UUID.randomUUID();
        var task = new TaskData(taskId);

        doReturn(Optional.of(task)).when(this.delegate).findTaskById(taskId);

        // when
        var optional = this.decorator.findTaskById(taskId);

        // then
        assertEquals(Optional.of(task), optional);

        verify(this.decorator).storeInCache(task);
    }

    @Test
    void findTaskById_TaskDoesNotExist_ReturnsEmptyOptional() {
        // given
        var taskId = UUID.randomUUID();

        // when
        var optional = this.decorator.findTaskById(taskId);

        // then
        assertEquals(Optional.empty(), optional);

        verify(this.decorator, never()).storeInCache(any());
    }
}

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

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

Статья на сайте

66 views·2 shares