Шпаргалка по мотивам Growing Object Oriented Software, Guided by Tests

Книга "Growing Object-Oriented Software Guided by Tests" содержит много примеров и рецептов того как правильно писать придерживаясь TDD. Отличная книга, очень советую. Но прочитав ее "на спех, в метро" я понял что скорее всего забуду практически все, по этому решил сделать шпаргалку по прочтенной книге. В этой статье только не большая часть, возможно будет еще одна статья касающаяся тестирования асинхронного кода.

Используй стандартную структура внутри теста

  • Setup. Приготовь среду для тестирования
  • Execute. Выполни тестируемый код
  • Verify. Проверь ожидаемое поведение или результат
  • Teardown. Если нужно, приведи среду в такое состояние которое не будет ломать остальные тесты
@Test
public void shouldAcceptRequestsIfLastWasDayAgo(){
        AcceptTimer acceptTimer = mock(AcceptTimer.class); // Setup
        Accepter accepter = new Accepter();                // Setup
        when(acceptTimer.isPreviousRequestWasDayAgo()).thenReturn(true); // Setup

        boolean isShouldAccept = accepter.isShouldBeAccepted(REQUEST) // Execute

        verify(accepter).isPreviousRequestWasDayAgo();                // Verify
        assertTrue("Request should be accepted only if 24h passed before previous one", 
        isShouldAccept); // Verify 
        // No Teardown

    }

Используй для тестов имена которые описывают что тестируется и какое ожидаемое поведение

Плохо:

public class ValidatorTest{
  @Test public void test1(){}
  @Test public void test2(){}
}

Лучше:

public class Validator{
  @Test public void shouldNotValidateIfUserNameIsEmpty(){}
  @Test public void shouldNotValidateIfAgelessThanOne(){}
}

Или так:

public class ValidatorShould{
  @Test public void notValidateIfUserNameIsEmpty(){}
  @Test public void notValidateIfAgelessThanOne(){}
}

Не стесняйся длинных названий тестовых методов, тест должны читается как нормальное предложение. Тесты это часть документации, должно быть понятно что они делают.

Делай тесты гибкими

Тестируй информацию, а не представление.

Плохо, мы полагаемся на форматирование toString:

String expectedAddress = "Divan str. 12";
assertEqual(addressBuilder.getAddress().toString(),expectedAddress);

Лучше:

Address expectedAddress = new Address("Divan",12);
assertEqual(addressBuilder.getAddress(), expectedAddress);

Если проверяешь коллекцию и в тесте не важна позиция, не полагайся на индекс.

Тест должен быть достаточно строгим для того что бы проверить логику и при этом гибким для того что бы не падать при изменении логики явно не касающейся теста. Изменение представления данных не должно ломать тест.

Не могу мокнуть зависимость

Ситуация возникает когда появляются не явные зависимости на Singleton или вызовы статических методов. Выходом является выделение таких зависимостей в отдельные сущности и объявление их явными зависимостями.

Пример, есть класс Accepter, у него есть метод isShouldBeAccepted, запрос должен быть разрешен для обработки если прошлый запрос был в течении суток. В нашем случае у класса Date есть не явная зависимость на System.currentTimeMillis().

public class Accepter {
    private Date lastRequestTime;

    public boolean isShouldBeAccepted(Request request){
        final Date now = new Date();
        if(isLastRequestWasDayAgo() || lastRequestTime == null){
            lastRequestTime = now;
            return true;
        }else{
            return false;
        }

    }
}

Тест:

@Test
public void shouldAcceptRequestsIfLastWasDayAgo(){
       boolean isAccepted = accepter.isShouldBeAccepted(FIRST_REQUEST);

        // wait for 24h... O_o

        assertTrue("Request should be accepted only if 24h passed before previous one",
                accepter.isShouldBeAccepted(SECOND_REQUEST));
}

Ждать 24 часа в тесте как то не хочется. Мы выделяем работу с временем в отдельную сущность, делаем ее явной зависимостью (используя конструктор) и работаем с ней в тестах.

public class Accepter {
    private final AcceptTimer timer;

    public Accepter(AcceptTimer timer) {
        this.timer = timer;
    }

    public boolean isShouldBeAccepted(Request request){
        return timer.isPreviousRequestWasDayAgo();
    }

}


@Test
public void shouldAcceptRequestsIfLastWasDayAgo(){
        AcceptTimer acceptTimer = mock(AcceptTimer.class);
        Accepter accepter = new Accepter();

        when(acceptTimer.isPreviousRequestWasDayAgo()).thenReturn(true);

        assertTrue("Request should be accepted only if 24h passed before previous one",
                accepter.isShouldBeAccepted(REQUEST));

    }


@Test
public void shouldNotAcceptRequestsIfLastWasNotDayAgo(){
        AcceptTimer acceptTimer = mock(AcceptTimer.class);
        Accepter accepter = new Accepter();

        when(acceptTimer.isPreviousRequestWasDayAgo()).thenReturn(false);

        assertFalse("Request should be accepted only if 24h passed before previous one",
                accepter.isShouldBeAccepted(REQUEST));

    }

Логирование это фича

Можно разделить 2 разновидности логирования:

  • Support logging (errors, info) - важен для бизнеса, может быть частью юзер интерфейса, может отслеживаться персоналом. Должен описывается как часть требований.
  • Diagnostic logging - важны для разработчиков, для дебага и поиска проблем.

От сюда выходит мысль что логирование (как часть Support logging) важно так же как и остальной код, и должно писаться в контексте TDD.

В этом контексте поможет использование оповещений вместо логирования, например вместо:

logger.info(format("User %s can't receive due to ...", user.userId));

Использовать:

support.notifyMoneyReceiveningProblemm(user);

suppport внутри должен раешить что сделать с сообщением и передать его конкретной реализации (отправить в другуй систему, положить в базу, или записать в файл);

Такой код легко тестировать, и мы отвязаны от конкретной реализации логирования.

Не мокай конкретные реализации

Мокая конкретные классы (и описывая дизайн программы таким образом) мы нарушаем Interface Segregation Princeiple потому что конкретный класс может содержать больше методов чем нужно тестируемой сущности, вместо этого мокай интерфейс который описывает только то что необходимо для данной сущности.

Вместо:

class DiningRoomShould{
    @Test
    public void giveFood(){
        Dog mockedDog = mock(Dog.class);
        dinningRoom.makeEatFor(mockedDog);

        verify(mockedDog).eat();
    }
}

class Dog{
    public void eat(){...}
    public void sleep(){...}
    public void sit(){...}
}

Используем:

interface Eating{
    public void eat();
}

class DiningRoomShould{
    @Test
    public void giveFood(){
        Eating eatingBeing = mock(Eating.class);
        dinningRoom.makeEatFor(eatingBeing);
        verify(eatingBeing).eat();
    }
}

class Dog implements Eating{
    public void eat(){...}
    public void sleep(){...}
    public void sit(){...}
}

Не мокай DTO

DTO не должны быть тупыми, желательно immutable.

Для того что бы мокать DTO просто нет причин, если сущность слишком сложная, создай для нее builder.

Сложная DTO - используй builder или factory method

Не очень:

new Order(new Customer("Vasy","Pupkin",29,new Address("Some streat",null)))

Лучше:

Order order = new OrderBuilder().fromCustomer(
            new CustomerBuiler()
                    .withName("Vasya")
                    .withSName("Pupkin")
                    .withAge(18)
                    .withAddress(new AddressBuilder()
                            .withStreat("Some streat")
                            .build()).build()).build();

Убираем шум передавая builder:

Order order = new OrderBuilder().fromCustomer(
            new CustomerBuiler()
                    .withName("Vasya")
                    .withSName("Pupkin")
                    .withAge(18)
                    .withAddress(new AddressBuilder()
                            .withStreat("Some streat"))).build();

Используя factory method:

Order order = anOrder().from(aCustomer()
                                  .withName("Vasya")
                                  .withSname("Pupkin")
                                  .withAge(18)
                                  .withAddress(anAddress().withStreat("Some streat"))).build();

Разбухающий конструктор

Ты пишешь тест и видишь что конструктор тестируемой сущности начинает расти и становится неприлично большим.

Есть несколько вариантов:

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

  • Возможно твои зависимости совсем не зависимости а просто значения по умолчанию ?

Пример второго случая:

class MusicFetcher{
    private final Downloader downloader;
    private String prefix;
    private String ext;
    private String defaultNameForUnnamed;
    private File pathToSave;

    MusicFetcher(Downloader downloader,
                String prefix,
                 String ext, 
                 String defaultNameForUnnamed, 
                 File pathToSave) {
        this.downloader = downloader;
        this.prefix = prefix;
        this.ext = ext;
        this.defaultNameForUnnamed = defaultNameForUnnamed;
        this.pathToSave = pathToSave;
    }

}

На самом деле зависимость тут Downloader, остальные параметры могут иметь значение по умолчанию и быть установлены позже:

class MusicFetcher{
    private final Downloader downloader;
    private String prefix = "fetched-";
    private String ext = "mp3";
    private String defaultNameForUnnamed = "Unnmaed";
    private File pathToSave = new File(".");

    MusicFetcher(Downloader downloader) { this.downloader = downloader; } 

    void setPrefix(String prefix) { this.prefix = prefix; }

    void setExt(String ext) { this.ext = ext; }

    void setDefaultNameForUnnamed(String defaultNameForUnnamed) { this.defaultNameForUnnamed = defaultNameForUnnamed; }

    void setPathToSave(File pathToSave) { this.pathToSave = pathToSave; }
}

Избегай большого количества verify в тестах

Большое количество verify вызовов в одном тесте говорит о том что юнит слишком сложный и его нужно разбить на несколько. Это как с слишком большим количеством assertion

Written on April 3, 2014