Быстрое
введение
в TDD
от А до Я


Бибичев Андрей
март 2012
@bibigine
                     Андрей Бибичев

• E-mail:       bibigine@gmail.com
• Twitter:      @bibigine
• Profile:      http://tinyurl.com/bibigine
• Slideshare:   http://www.slideshare.net/bibigine
Для кого и зачем?
• Beginner
 o хорошая точка входа


• Intermediate
 o поможет лучше всё структурировать в голове и
   объяснять коллегам


• Advanced
 o можно использовать для обучения и проверки
   других
Введение:
Data-Driven тесты


                А
• Java                  • C++
 o JUnit                 o Google Test
 o Mockito               o Google Mock
 o [FEST]
                        • PHP
• .Net (C#)              o phpUnit
 o MS Unit
 o Moq
 o [FluentAssertions]
Пример
Округление цены
 $19.99
 $99.99
 $159
 $299
 $999
 $1095
 $9995
TDD:
классический пример


                 Б
http://www.objectmentor.com/resources/articles/xpepisode.htm

http://wiki.agiledev.ru/doku.php?id=tdd:bowling

http://www.slideshare.net/stewshack/bowling-game-kata-c
BowlingGame

roll(pins: int)
getScore(): int
getFrameScore(indx: int): int
getCurrentFrame(): int
Тесты
1.   На 0 (ничего не сбито)
2.   Простой фрейм
3.   Очки по фреймам
4.   Номер текущего фрейма
5.   Spare
6.   Strike
7.   Spare в 10-ом фрейме
8.   Strike в 10-ом фрейме
Цикл разработки в TDD
                   Написание неработающего теста
                    для новой функциональности


                              RED



            REFACTOR                        GREEN

   Рефакторим код, чтобы               Пишем ровно столько кода,
тесты продолжили проходить,              чтобы тест прошел
     а код стал чистым
Традиционный цикл разработки

                        Пишем сразу заметный
                       кусок функциональности


                            CODING




        DEBUGGING                          COMPILING


   Отлаживаем код,                             Добиваемся
  ловим и исправляем                        компилируемости
  поверхностные баги
TDD:
состояние vs поведение


                  В-Э
Min  Max
                    Кол-
             День          Cycle Cycle
                     во
                           Time Time
              1      0      -      -
        N     2      0      -      -
              3      2      3      3
              4      3      3      4
              5      1      4      4




Cycle Time
Контрольный пример:

                            Min  Max
                    Кол-
             День          Cycle Cycle
                     во
                           Time Time
              1      0      -      -
              2      0      -      -
         5    3      0      -      -
              4      0      -      -
              5      1      5      5
Диаграмма классов
Conveyor

tick(Item[*]):Item[*]

             workers
         *
       Worker                         Item




                       queue
  enqueue(Item[*])             * lifeTime():int
  tick():Item[*]                tick()
Conveyor




 Item




Worker
Conveyor

tick(Item[*]):Item[*]

             workers
         *
       Worker                         Item




                       queue
  enqueue(Item[*])             * lifeTime():int
  tick():Item[*]                tick()
У вновь созданной детали
время жизни должно равняться нулю


           дано: новая деталь,
 когда: запрашиваем у нее время жизни,
     тогда: получаем в результате 0
import org.junit.Test;
import static org.fest.assertions.Assertions.assertThat;

public class ItemTest {
    @Test
    public void shouldHaveZeroLifeTimeAterCreation() {
        // given
        final Item item = new Item();
        // when
        int lifeTime = item.lifeTime();
        // then
        assertThat(lifeTime).isZero();
    }




                                                 FEST
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using FluentAssertions;

[TestClass]
public class ItemTest
{
    [TestMethod]
    public void ShouldHaveZeroLifeTimeAterCreation()
    {
        // given
        var item = new Item();
        // when
        var lifeTime = item.LifeTime;
        // then
        lifeTime.Should().Be(0);
    }

                                     FluentAssertions
Это «вырожденный»
 тест на состояние
public class Item {
    public int lifeTime() {
        return 0;
    }
}




public class Item
{
    public int LifeTime { get; }
}
Дано:

   арбуз + гиря 1 кг = гиря 6 кг
   арбуз = ?


Решение:

        x+1=6
        x=6–1=5


Ответ: 5 кг
vs.
Test…                    Should…
{                        {
    // Arrange               // GIVEN
    …                        …
    // Action                // WHEN
    …                        …
    // Assertion             // THEN
    …                        …
}                        }
Март 2006
Журнал «Better Software»
Статья «Introducing BDD»
       Dan North
Альтернатива
  ROY OSHEROVE
ИмяМетода_Условия_Результат
[TestMethod]
public void IsValidLogFileName_ValidFile_ReturnsTrue()
{
    // arrange
    var analyzer = new LogAnalyzer();

    // act
    var result = analyzer.IsValidLogFileName("whatever.slf");

    // assert
    Assert.IsTrue(result, "filename should be valid!");
}



                     LogAnalyzer

     + IsValidLogFileName(name : string) : bool
Критерий
    хорошо оформленного теста:
•   Содержательное название
•   Короткое тело (max = 20-30 строк)
•   По шаблону AAA или GIVEN-WHEN-THEN
•   Без циклов
•   Без ветвлений (if-ов и case-ов)

• Должен легко читаться
  (literate programming)
Оповещение о том, что
     прошел такт конвейера
      должно увеличивать
значение времени жизни на один


          дано: новая деталь,
когда: оповещаем ее о такте конвейера,
   тогда: время жизни становится 1
@Test
public void shouldIncrementLifeTimeDuringTick() {
    // given
    final Item item = new Item();
    // when
    item.tick();
    // then
    assertThat(item.lifeTime()).isEqualTo(1);
}
[TestMethod]
public void ShouldIncrementLifeTimeDuringTick()
{
    // given
    var item = new Item();
    // when
    item.Tick();
    // then
    item.LifeTime.Should().Be(1);
}
Это примитивный пример
   теста на состояние
public class Item {
    public int lifeTime() { return lifeTime; }
    public void tick() { lifeTime++; }
    private int lifeTime;
}



public class Item
{
    public int LifeTime { get; private set; }
    public void Tick() { LifeTime++; }
}
Conveyor

tick(Item[*]):Item[*]

             workers
         *
       Worker                         Item




                       queue
  enqueue(Item[*])             * lifeTime():int
  tick():Item[*]                tick()
Worker




queue
Рабочий ничего не обрабатывает,
       если нет деталей

              x

  У вновь созданного рабочего
 входная очередь деталей пуста
public class WorkerTest {
    @Test
    public void shouldReturnNothingIfNothingToDo() {
        // given
       final Worker worker = new Worker();
       // when
       final List<Item> output = worker.tick();
       // then
       assertThat(output).isEmpty();
}
[TestClass]
public class WorkerTest
{
    [TestMethod]
    public void ShouldReturnNothingIfNothingToDo()
    {
        // given
        var worker = new Worker();
        // when
        var output = worker.Tick();
        // then
        output.Should().BeEmpty();
    }
Если во время обработки
 на кубике выпало значение
большее количества деталей
          в очереди,
  то рабочий обрабатывает
    все детали в очереди
      (и больше ничего)
Conveyor

tick(Item[*]):Item[*]

             workers
         *
       Worker                         Item




                       queue
  enqueue(Item[*])             * lifeTime():int
  tick():Item[*]                tick()
             dice
         1
        Dice

    roll():int
Dice   Worker




queue
Dependency Injection (DI)
  через конструктор



         Worker
    Worker(Dice)
    enqueue(Item[*])
    tick():Item[*]
@Test
public void shouldProcessNotGreaterThanItemsInQueue() {
    // given
    final Dice dice = createDiceStub(4);
    final Worker worker = new Worker(dice);

    final List<Item> items = Arrays.asList(
                             new Item(), new Item());
    worker.enqueue(items);

    // when
    final List<Item> output = worker.tick();

    // then
    assertThat(output).isEqualTo(items);
}
[TestMethod]
public void ShouldProcessNotGreaterThanItemsInQueue()
{
    // given
    var dice = CreateDiceStub(4);
    var worker = new Worker(dice);

    var items = new[] { new Item(), new Item() };
    worker.Enqueue(items);

    // when
    var output = worker.Tick();

    // then
    output.Should().Equal(items);
}
4



enqueue   tick      Dice

                 roll():int




                  DiceStub

                 roll():int
import static org.mockito.Mockito.*;
…

private Dice createDiceStub(int rollValue) {
    final Dice dice = mock(Dice.class);
    when(dice.roll()).thenReturn(rollValue);
    return dice;
}



using Moq;
…

private Dice CreateDiceStub(int rollValue) {
    var diceMock = new Mock<Dice>();
    diceMock.Setup(d => d.Roll()).Returns(rollValue);
    return diceMock.Object;
}
Хотя мы и воспользовались
          mock-объектом,
это всё равно, по большому счету,
         тест на состояние
Если во время обработки
на кубике выпало значение N,
 меньше количества деталей
         в очереди,
  то обрабатывается только
первые N деталей из очереди
@Test
public void shouldProcessNotGreaterThanRolledValue() {
    // given
    final int rollValue = 3;
    final Dice dice = createDiceStub(rollValue);
    final Worker worker = new Worker(dice);

    final List<Item> items = Arrays.asList(
        new Item(), new Item(), new Item(), new Item());
    worker.enqueue(items);

    // when
    final List<Item> output = worker.tick();

    // then
    assertThat(output).isEqualTo(
        items.subList(0, rollValue));
}
Еще аналогичные тесты:
• Проверяем, что enqueue() добавляет в
  очередь

• Проверяем, что tick() удаляет из очереди
  обработанные детали
Тупой, но важный тест:

     Во время Worker.tick()
        кубик бросается
        ровно один раз!
Во время тренингов
    доводилось встречать такой код:

public List<Item> tick() {
    final List<Item> output = new LinkedList<Item>();

    for (int i = 0; i < dice.roll(); i++) {

        if (queue.isEmpty()) break;
        output.add(queue.poll());
    }

    return output;
}
Всё работает,
       но распределение…
P(1) = 1/6 = 0.1(6)
P(2) = 5/18 = 0.2(7)
P(3) = 5/18 = 0.2(7)
P(4) = 5/27 = 0.(185)
P(5) = 25/324  0.077
P(6) = 5/324  0.0154
@Test
public void shouldRollDiceOnlyOnceDuringTick() {
    // given
    final Dice dice = createDiceStub(3);
    final Worker worker = new Worker(dice);

    final List<Item> items = Arrays.asList(
        new Item(), new Item(),
        new Item(), new Item());
    worker.enqueue(items);

    // when
    worker.tick();

    // then
    verify(dice, times(1)).roll();
}
[TestMethod]
public void ShouldRollDiceOnlyOnceDuringTick()
{
    // given
    var diceMock = new Mock<Dice>();
    diceMock.Setup(d => d.Roll()).Returns(3);
    var dice = diceMock.Object;

    var worker = new Worker(dice);

    var items = new[] { new Item(), new Item(),
                        new Item(), new Item() };
    worker.Enqueue(items);

    // when
    worker.Tick();

    // then
    diceMock.Verify(d => d.Roll(), Times.Once());
}
Это примитивный пример
      теста на поведение:
мы проверили как взаимодействует
  наш объект с другим объектом
На состояние          На поведение


               mock




    Объект              Объект
Закрепим материал:
• Проверим, что во время Worker.tick()
  вызывается Item.tick() для всех деталей,
  находящихся в очереди на начало
  tick()-а

• Это можно проверить, не прибегая к
  mock-ам – через значение lifeTime(), но
  тогда мы тестируем два класса сразу, а
  не один в изоляции
@Test
public void shouldCallTickForAllItemsInQueue() {
    // given
    final Dice dice = createDiceStub(1);
    final Worker worker = new Worker(dice);

    final Item firstItem = mock(Item.class);
    final Item secondItem = mock(Item.class);

    final List<Item> items = Arrays.asList(
        firstItem, secondItem);
    worker.enqueue(items);

    // when
    worker.tick();

    // then
    verify(firstItem, times(1)).tick();
    verify(secondItem, times(1)).tick();
}
[TestMethod]
public void ShouldCallTickForAllItemsInQueue()
{
    // given
    var dice = CreateDiceStub(1);
    var worker = new Worker(dice);

    var firstItemMock = new Mock<Item>();
    var secondItemMock = new Mock<Item>();

    worker.Enqueue(new[] { firstItemMock.Object,
                           secondItemMock.Object });

    // when
    worker.Tick();

    // then
    firstItemMock.Verify(i => i.Tick(), Times.Once());
    secondItemMock.Verify(i => i.Tick(), Times.Once());
}
Совет «по случаю»
•   Избегайте имен переменных item1, item2 и т.п.
•   Точно запутаетесь и опечатаетесь
•   Лучше говорящие имена
•   Или на худой конец: firstItem, secondItem и т.п.
Переходим к самому
   интересному
Conveyor

tick(Item[*]):Item[*]

             workers
         *
       Worker                         Item




                       queue
  enqueue(Item[*])             * lifeTime():int
  tick():Item[*]                tick()
             dice
         1
        Dice

    roll():int
Как тестировать?
Тесты на состояние:
• Можно придумать несколько тестовых
  сценариев (разрисовать на бумажке)
  o в стиле «Контрольный пример» из начала презентации


• Проблемы:
  o Но как они помогут написать реализацию?
  o Какие тесты написать первыми, а какие потом?
  o Как быть уверенным, что протестированы все случаи и
    нюансы? (полнота покрытия)
  o Как эти тесты будут соотноситься со спецификацией?
    (test == executable specification)
Напомним спецификацию:
1. То, что подается на вход конвейера, сражу же
   оказывается в очереди первого рабочего, т.е. до
   начала обработки им деталей

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

3. Для всех остальных рабочих их результат работы
   попадает в очередь к следующему рабочему,
   но уже после того, как тот произвел обработку
Min  Max
                    Кол-
             День          Cycle Cycle
                     во
                           Time Time
              1      0      -      -
        N     2      0      -      -
              3      2      3      3
              4      3      3      4
              5      1      4      4




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

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

3. Для всех остальных рабочих их результат работы
   попадает в очередь к следующему рабочему,
   но уже после того, как тот произвел обработку
worker1        worker2




1. enqueue( )
2. tick




                       conveyor             Conveyor
   tick( )

                                      Conveyor(Worker[*])
                                      tick(Item[*]):Item[*]
@Test
public void shouldEnqueueInputToFirstWorkerBeforeProcessing() {
    // given
    final List<Item> someInput = Arrays.asList(
             new Item(), new Item(), new Item());

    final Worker firstWorker = mock(Worker.class);
    final Worker secondWorker = mock(Worker.class);
    final List<Worker> workers = Arrays.asList(
            firstWorker, secondWorker);

    final Conveyor conveyor = new Conveyor(workers);

    // when
    conveyor.tick(someInput);

    // then
    final InOrder order = inOrder(firstWorker);
    order.verify(firstWorker, times(1)).enqueue(someInput);
    order.verify(firstWorker, times(1)).tick();
}
[TestMethod]
public void ShouldEnqueueInputToFirstWorkerBeforeProcessing() {
    // given
    var someInput = new[] { new Item(), new Item };

    var callSequence = MoqSequence.Create();
    var firstWorkerMock = new Mock<Worker>();
    firstWorkerMock.Setup(w => w.Enqueue(someInput))
        .InSequence(callSequence);
    firstWorkerMock.Setup(w => w.Tick())
        .InSequence(callSequence);
    var firstWorker = firstWorkerMock.Object;

    var secondWorker = new Mock<Worker>().Object;

    var conveyor = new Conveyor(new[]{firstWorker, secondWorker });

    // when
    conveyor.Tick(someInput);

    // then
    callSequence.Verify();
}
Недостаток Moq:
нет «встроенной» поддержки
последовательностей вызовов




                  Moq.Sequences.dll
public static class MoqSequence {
    public interface ISequence { void Verify(); }
    public static ISequence Create() { return new Sequence(); }
    public static void InSequence(this ICallback mock,
                                  ISequence sequence) {
        var seq = (Sequence)sequence;
        var id = seq.GetNextCallId();
        mock.Callback(() => seq.LogCall(id));
    }

    private class Sequence : ISequence {
        public int GetNextCallId() { return nextCallId++; }
        public void LogCall(int id) { calls.Add(id); }
        public void Verify() {
            calls.Count.Should().Be(nextCallId,
                "it's expected count of calls in sequence");
            for (var i = 0; i < calls.Count; i++) {
                calls[i].Should().Be(i,
                    "wrong call sequence in position {0} ", i);
            }
        }
        private int nextCallId;
        private readonly IList<int> calls = new List<int>();
    }
}
1. То, что подается на вход конвейера, сражу же
   оказывается в очереди первого рабочего, т.е. до
   начала обработки им деталей

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

3. Для всех остальных рабочих их результат работы
   попадает в очередь к следующему рабочему,
   но уже после того, как тот произвел обработку
@Test
public void shouldReturnOutputOfLastWorker() {
    // given
    final List<Item> someOutput = Arrays.asList(
             new Item(), new Item());
    final Worker firstWorker = mock(Worker.class);
    final Worker secondWorker = mock(Worker.class);
    when(secondWorker.tick()).thenReturn(someOutput);

    final List<Worker> workers = Arrays.asList(
            firstWorker, secondWorker);
    final Conveyor conveyor = new Conveyor(workers);

    final List<Item> someInput = Arrays.asList(new Item());

    // when
    final List<Item> output = conveyor.tick(someInput);

    // then
    assertThat(output).isEqualTo(someOutput);
}
Это можно было проверить,
     создав реальных Worker-ов,
«накормив» заранее второго нужными
              Item-ами,
 но такие тесты уже больше похожи
         на интеграционные
Mock-и очень удобны,
  чтобы имитировать любое
    необходимое состояние
      стороннего объекта,
   не связываясь с длинной
цепочкой вызовов, необходимой
  для приведения реального
    объекта в это состояние
Fake   Mock   Stub, Dummy
1. То, что подается на вход конвейера, сражу же
   оказывается в очереди первого рабочего, т.е. до
   начала обработки им деталей

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

3. Для всех остальных рабочих их результат работы
   попадает в очередь к следующему рабочему,
   но уже после того, как тот произвел обработку
conveyor                firstWorker   secondWorker   thirdWorker


tick()
                 enqueue()

                  tick()

             
                  tick()

             
                 enqueue()

                  tick()

             
                 enqueue()

  
@Test public void
shouldEnqueueOutputOfPreviousWorkerToTheNextAfterProcessing() {
    // given
    final List<Item> outputOfFirstWorker = Arrays.asList(
             new Item(), new Item());
    final List<Item> outputOfSecondWorker = Arrays.asList(
             new Item(), new Item(), new Item());

    final Worker firstWorker = mock(Worker.class);
    when(firstWorker.tick()).thenReturn(outputOfFirstWorker);

    final Worker secondWorker = mock(Worker.class);
    when(secondWorker.tick()).thenReturn(outputOfSecondWorker);

    final Worker thirdWorker = mock(Worker.class);

    final List<Worker> workers = Arrays.asList(
            firstWorker, secondWorker, thirdWorker);

    final Conveyor conveyor = new Conveyor(workers);

    final List<Item> someInput = Arrays.asList(new Item());
// when
    conveyor.tick(someInput);

    // then
    InOrder secondWorkerOrder = inOrder(secondWorker);
    secondWorkerOrder.verify(secondWorker).tick();
    secondWorkerOrder.verify(secondWorker)
        .enqueue(outputOfFirstWorker);

    InOrder thirdWorkerOrder = inOrder(thirdWorker);
    thirdWorkerOrder.verify(thirdWorker).tick();
    thirdWorkerOrder.verify(thirdWorker)
        .enqueue(outputOfSecondWorker);
}
Тест на поведение – это проверка,
что код соответствует задуманной
 диаграмме последовательности
Реализация
public List<Item> tick(final List<Item> input) {
    if (workers.isEmpty()) { return input; }

    final Worker firstWorker = workers.get(0);
    firstWorker.enqueue(input);
    List<Item> output = firstWorker.tick();

    for (int i = 1; i < workers.size(); i++) {
        final Worker worker = workers.get(i);
        final List<Item> tmp = worker.tick();
        worker.enqueue(output);
        output = tmp;
    }

    return output;
}
public virtual IList<Item> Tick(IList<Item> input)
{
    if (workers.Count == 0) return input;

    var firstWorker = workers.First();
    firstWorker.Enqueue(input);
    var lastOutput = firstWorker.Tick();

    foreach (var worker in workers.Skip(1))
    {
        var tmp = worker.Tick();
        worker.Enqueue(lastOutput);
        lastOutput = tmp;
    }

    return lastOutput;
}
Полный код
http://tinyurl.com/tdd-demo-for-agiledays

                 17 Mb
• Java
  o junit + mockito + fest
  o проект NetBeans 7.0

• .Net (C#)
  o ms unit + moq + fluentAssertions
  o проект VS 2010

• C++
  o google test + google mock
  o проект VS 2010

• PHP
  o phpUnit
  o проект PhpStorm 3.0
WARNING:
          В коде не выделены
 интерфейсы для Item, Dice и Worker
только в целях «упрощения» примера.

      Выделение интерфейсов
предпочтительнее перекрытия самих
   классов с функциональностью.

      «interface»
         Dice         RandomDice

    roll():int
Итого
Плюсы тестов на поведение
• Просто писать (когда привыкнешь)
  o не нужно долго и мучительно приводить окружение в нужное
    состояние
• Являются истинными unit-тестами
  o проверяют функционал класса в изоляции от всех остальных
• Хорошо отражают спецификации и дают
  уверенность в хорошем покрытии кода
  o executable specification
• Принуждают к модульному дизайну
  o SRP, LSP, DIP, ISP
• Позволяют разрабатывать функционал
  сверху-вниз от сценариев использования
  o а не снизу вверх от данных
1        Conveyor

tick(Item[*]):Item[*]

                 workers
             *
    2       Worker
                                     3    Item




                           queue
    enqueue(Item[*])               * lifeTime():int
    tick():Item[*]                  tick()
                 dice
             1
        4   Dice

        roll():int
Минусы тестов на поведение
• Чтобы ими овладеть, требуется ментальный
  сдвиг
• Проверяют, что код работает так, как вы
  ожидаете, но это не значит, что он работает
  правильно
  o этот недостаток легко снимается небольшим количеством
    интеграционных тестов
• Не весь функционал можно так
  протестировать
• Тесты хрупкие
  o изменение в реализации ломает тесты
• Требуют выделения интерфейсов или
  виртуальности методов
  o Обычно не проблема. А если проблема, то используйте «быстрые
    mock-и» на C++ templates
Mock Hell
• Чтобы его избежать,
  очень важно соблюдать ISP
  o Широко используемыми могут быть только
    стабильные интерфейсы
Mockist vs. Classicist



Mockist + Classicist
Legacy-код


             Ю
Пример
by Miško Hevery

  http://bit.ly/92Ozrz
 http://goo.gl/V0aWx
public class InboxSyncer {
   private static final InboxSyncer instance
                              = new InboxSyncer(Config.get());
   public static getInstance() { return instance; }

   private   final Certificate cert;
   private   final String username;
   private   final String password;
   private   long lastSync = -1;

   private InboxSyncer(Config config) {
       this.cert = DESReader.read(config.get(“cert.path”));
       User user = config.getUser();
       this.username = user.getUsername();
       this.password = user.getPassword();
   }
public sync() {
        long syncFrom = lastSync;
        if (syncFrom == -1) {
            syncFrom = new Date().getTime() - Defaults.INBOX_SYNC_AMOUNT;
        }
        Inbox inbox = Inbox.get(username);
        POPConnector pop = new POPConnector(cert, username, password);
        pop.connect();
        try {
            Iterator<Messages> messages = pop.messagesIterator();
            Message message;
            while ( messages.hasNext() &&
                    (message = messages.next()).getTime() > syncFrom ) {
                if (Defaults.FILTER_SPAM
                        ? MessageFilter.spam(message)
                        : MessageFilter.addressedToMe(message, username)) {
                    this.lastSync = Math.max(this.lastSync, message.getTime());
                    if (!inbox.contains(message.getId()) {
                        inbox.add(message);
                    }
                }
            }
        } finally {
            pop.disconnect();
        }
    }
}
Если писать на «это»
     тест в лоб
public class InboxSyncerTest extends TestCase {
   PopServer server;
   Certificate cert;
   static InboxSyncer syncer;

  public setUp() {
      if (syncer == null) {
          Config config = Config.get();
          User user = new User();
          user.setUsername(“test@example.com”);
          user.setPassword(“myTestPassword”);
          config.setUser(user);
          cert = DESCert.generateCert();
          File tempCert = File.creteTempFile(“temp”, “.cert”);
          FileOutputStream out = new FileOutputStream(tempCert);
          out.write(cert);
          out.close();
          config.set(“cert.path”, tempCert.toString());
          syncer = InboxSyncer.get();
      }

      // Reset the state of Global Objects
      Inbox inbox = Inbox.get(“test@example.com”);
      Iterator<Message> messages = inbox.getMessages();
      while(messages.hasNext()) {
          messages.remove();
      }

      Reflection.setPrivateField(syncer, “lastSync”, -1);

      POPServer server = new POPServer(cert);
      server.start();

      MessageQueue queue = server.getMessageQueueForUser(“test@example.com”);
      queue.clear();
  }
public tearDown() {
        server.stop();
    }

    public testSync() {
        Defaults.FILTER_SPAM = true;
        MessageQueue queue = server.getMessageQueueForUser(“test@example.com”);
        Date time = new Date();
        Message message = new Message(“from”, “to”, “subject”, “message”, time);
        queue.addMessage(message);
        syncer.sync();
        Inbox inbox = Inbox.get(“test@example.com”);
        assertTrue(inbox.contains(message.getId());
        assertEquals(1, inbox.getMessages().size());
    }
}
Что не так с кодом?
•   Глобальный контекст (Singleton)
•   Нарушение закона Деметры (Law of Demeter)
•   Использование текущего системного времени
•   Самостоятельное создание объектов
•   Отсутствие Dependency Injection
public class InboxSyncer {
   private final POPConnector pop;
   private final Inbox inbox;
   private final Filter filter;
   private final Date lastSync;
   private final long defaultPastSync;

  private InboxSyncer(POPConnector pop, Inbox inbox, Filter filter,
                      Date lastSync, long defaultPastSync) {
      this.pop = pop;
      this.inbox = inbox;
      this.filter = filter;
      this.lastSync = lastSync;
      this.defaultPastSync = defaultPastSync;
  }
public sync(Date now) {
        Date syncFrom = lastSync;
        if (syncFrom == null) {
            syncFrom = new Date(now.getTime() - defaultPastSync);
        }
        pop.connect();
        try {
            Iterator<Messages> messages = pop.messagesIterator();
            Message message;
            while ( messages.hasNext() &&
                    (message = messages.next()).getTime() > syncFrom.getTime() ) {
                if (filter.apply(message)) {
                    this.lastSync = new Date(Math.max(
                                         this.lastSync.getTime(), message.getTime()));
                    if (!inbox.contains(message.getId()) {
                        inbox.add(message);
                    }
                }
            }
        } finally {
            pop.disconnect();
        }
    }
}
Теперь тесты:
сlass InboxSyncerSpec extends TestCase {
   Date longEgo = new Date(1);
   Date past = new Date(2);
   Date now = new Date(3);
   Date future = new Date(4);
   POPConnector pop = new MockPOPConnector();
   Inbox inbox = new Inbox();
   Filter filter = new NoopFilter();
   Message longEgoMsg = new Message(“from”, “to”, “subject”, “msg”, longEgo);
   Message pastMsg = new Message(“me”, “you”, “hello”, “world”, past);
   long noDefaultSync = -1;

   public @Test itShouldSyncMessagesFromPopSinceLastSync() {
       pop.addMessage(longEgoMsg);
       pop.addMessage(pastMsg);
       new InboxSyncer(pop, inbox, filter, longEgo, noDefaultSync).sync(now);
       assertThat(inbox.getMessages(), is(equalTo(pastMsg)));
       assertThat(pop.isClosed(), is(true));
   }
public @Test itShouldCloseConnectionEvenWhenExceptionThrown() {
   Exception exception = new Exception();
   Filter filter = new ExceptionThrowingFilter(exception);
   InboxSyncer syncer = new InboxSyncer(pop, null, filter, null, noDefaultSync);
   try {
       syncer.sync(now);
       fail(“Exception expected!”);
   } catch (Exception e) {
       assertThat(e, is(exception));
       assertThat(pop.isClosed(), is(true));
   }
}

public @Test itShouldSyncMessagesOnlyWhenNotAlreadyInInbox() {
    pop.addMessage(pastMsg);
    inbox.add(pastMsg);
    new InboxSyncer(pop, inbox, filter, longEgo, noDefaultSync).sync(now);
    assertThat(inbox.getMessages(), is(equalTo(pastMsg)));
}

public @Test itShouldIgnoreMessagesOlderThenLastSync() {}

public @Test itShouldIgnoreMessagesFailingFilter() {}

public @Test itShouldDefaultToDefaultTimeWhenNeverSynced() {}   }
Тестопригодная
  архитектура


                 Я
http://www.youtube.com/watch?v=WpkDN78P884
DCI

Data, context and interaction

    http://architects.dzone.com/videos/dci-architecture-trygve
        http://www.artima.com/articles/dci_vision.html
Смежные
выступления


          bonus
Роль декомпозиции функционала на
отдельные классы при следовании TDD
Архитектура в Agile:
слабая связность кода
Тесты на поведение против
    тестов на состояние
Спасибо за внимание!

        Вопросы?

              @bibigine
        bibigine@gmail.com
      http://tinyurl.com/bibigine
  http://www.slideshare.net/bibigine

Быстрое введение в TDD от А до Я

  • 1.
    Быстрое введение в TDD от Адо Я Бибичев Андрей март 2012
  • 2.
    @bibigine Андрей Бибичев • E-mail: [email protected] • Twitter: @bibigine • Profile: http://tinyurl.com/bibigine • Slideshare: http://www.slideshare.net/bibigine
  • 3.
    Для кого изачем? • Beginner o хорошая точка входа • Intermediate o поможет лучше всё структурировать в голове и объяснять коллегам • Advanced o можно использовать для обучения и проверки других
  • 4.
  • 5.
    • Java • C++ o JUnit o Google Test o Mockito o Google Mock o [FEST] • PHP • .Net (C#) o phpUnit o MS Unit o Moq o [FluentAssertions]
  • 6.
    Пример Округление цены $19.99 $99.99 $159 $299 $999 $1095 $9995
  • 7.
  • 9.
  • 11.
  • 12.
    Тесты 1. На 0 (ничего не сбито) 2. Простой фрейм 3. Очки по фреймам 4. Номер текущего фрейма 5. Spare 6. Strike 7. Spare в 10-ом фрейме 8. Strike в 10-ом фрейме
  • 13.
    Цикл разработки вTDD Написание неработающего теста для новой функциональности RED REFACTOR GREEN Рефакторим код, чтобы Пишем ровно столько кода, тесты продолжили проходить, чтобы тест прошел а код стал чистым
  • 14.
    Традиционный цикл разработки Пишем сразу заметный кусок функциональности CODING DEBUGGING COMPILING Отлаживаем код, Добиваемся ловим и исправляем компилируемости поверхностные баги
  • 15.
  • 16.
    Min Max Кол- День Cycle Cycle во Time Time 1 0 - - N 2 0 - - 3 2 3 3 4 3 3 4 5 1 4 4 Cycle Time
  • 17.
    Контрольный пример: Min Max Кол- День Cycle Cycle во Time Time 1 0 - - 2 0 - - 5 3 0 - - 4 0 - - 5 1 5 5
  • 18.
  • 19.
    Conveyor tick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick()
  • 20.
  • 21.
    Conveyor tick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick()
  • 22.
    У вновь созданнойдетали время жизни должно равняться нулю дано: новая деталь, когда: запрашиваем у нее время жизни, тогда: получаем в результате 0
  • 23.
    import org.junit.Test; import staticorg.fest.assertions.Assertions.assertThat; public class ItemTest { @Test public void shouldHaveZeroLifeTimeAterCreation() { // given final Item item = new Item(); // when int lifeTime = item.lifeTime(); // then assertThat(lifeTime).isZero(); } FEST
  • 24.
    using System; using Microsoft.VisualStudio.TestTools.UnitTesting; usingFluentAssertions; [TestClass] public class ItemTest { [TestMethod] public void ShouldHaveZeroLifeTimeAterCreation() { // given var item = new Item(); // when var lifeTime = item.LifeTime; // then lifeTime.Should().Be(0); } FluentAssertions
  • 25.
  • 26.
    public class Item{ public int lifeTime() { return 0; } } public class Item { public int LifeTime { get; } }
  • 28.
    Дано: арбуз + гиря 1 кг = гиря 6 кг арбуз = ? Решение: x+1=6 x=6–1=5 Ответ: 5 кг
  • 29.
    vs. Test… Should… { { // Arrange // GIVEN … … // Action // WHEN … … // Assertion // THEN … … } }
  • 30.
    Март 2006 Журнал «BetterSoftware» Статья «Introducing BDD» Dan North
  • 31.
  • 32.
    ИмяМетода_Условия_Результат [TestMethod] public void IsValidLogFileName_ValidFile_ReturnsTrue() { // arrange var analyzer = new LogAnalyzer(); // act var result = analyzer.IsValidLogFileName("whatever.slf"); // assert Assert.IsTrue(result, "filename should be valid!"); } LogAnalyzer + IsValidLogFileName(name : string) : bool
  • 33.
    Критерий хорошо оформленного теста: • Содержательное название • Короткое тело (max = 20-30 строк) • По шаблону AAA или GIVEN-WHEN-THEN • Без циклов • Без ветвлений (if-ов и case-ов) • Должен легко читаться (literate programming)
  • 35.
    Оповещение о том,что прошел такт конвейера должно увеличивать значение времени жизни на один дано: новая деталь, когда: оповещаем ее о такте конвейера, тогда: время жизни становится 1
  • 36.
    @Test public void shouldIncrementLifeTimeDuringTick(){ // given final Item item = new Item(); // when item.tick(); // then assertThat(item.lifeTime()).isEqualTo(1); }
  • 37.
    [TestMethod] public void ShouldIncrementLifeTimeDuringTick() { // given var item = new Item(); // when item.Tick(); // then item.LifeTime.Should().Be(1); }
  • 38.
    Это примитивный пример теста на состояние
  • 39.
    public class Item{ public int lifeTime() { return lifeTime; } public void tick() { lifeTime++; } private int lifeTime; } public class Item { public int LifeTime { get; private set; } public void Tick() { LifeTime++; } }
  • 40.
    Conveyor tick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick()
  • 41.
  • 42.
    Рабочий ничего необрабатывает, если нет деталей x У вновь созданного рабочего входная очередь деталей пуста
  • 43.
    public class WorkerTest{ @Test public void shouldReturnNothingIfNothingToDo() { // given final Worker worker = new Worker(); // when final List<Item> output = worker.tick(); // then assertThat(output).isEmpty(); }
  • 44.
    [TestClass] public class WorkerTest { [TestMethod] public void ShouldReturnNothingIfNothingToDo() { // given var worker = new Worker(); // when var output = worker.Tick(); // then output.Should().BeEmpty(); }
  • 45.
    Если во времяобработки на кубике выпало значение большее количества деталей в очереди, то рабочий обрабатывает все детали в очереди (и больше ничего)
  • 46.
    Conveyor tick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick() dice 1 Dice roll():int
  • 47.
    Dice Worker queue
  • 48.
    Dependency Injection (DI) через конструктор Worker Worker(Dice) enqueue(Item[*]) tick():Item[*]
  • 49.
    @Test public void shouldProcessNotGreaterThanItemsInQueue(){ // given final Dice dice = createDiceStub(4); final Worker worker = new Worker(dice); final List<Item> items = Arrays.asList( new Item(), new Item()); worker.enqueue(items); // when final List<Item> output = worker.tick(); // then assertThat(output).isEqualTo(items); }
  • 50.
    [TestMethod] public void ShouldProcessNotGreaterThanItemsInQueue() { // given var dice = CreateDiceStub(4); var worker = new Worker(dice); var items = new[] { new Item(), new Item() }; worker.Enqueue(items); // when var output = worker.Tick(); // then output.Should().Equal(items); }
  • 51.
    4 enqueue tick Dice roll():int DiceStub roll():int
  • 52.
    import static org.mockito.Mockito.*; … privateDice createDiceStub(int rollValue) { final Dice dice = mock(Dice.class); when(dice.roll()).thenReturn(rollValue); return dice; } using Moq; … private Dice CreateDiceStub(int rollValue) { var diceMock = new Mock<Dice>(); diceMock.Setup(d => d.Roll()).Returns(rollValue); return diceMock.Object; }
  • 54.
    Хотя мы ивоспользовались mock-объектом, это всё равно, по большому счету, тест на состояние
  • 55.
    Если во времяобработки на кубике выпало значение N, меньше количества деталей в очереди, то обрабатывается только первые N деталей из очереди
  • 56.
    @Test public void shouldProcessNotGreaterThanRolledValue(){ // given final int rollValue = 3; final Dice dice = createDiceStub(rollValue); final Worker worker = new Worker(dice); final List<Item> items = Arrays.asList( new Item(), new Item(), new Item(), new Item()); worker.enqueue(items); // when final List<Item> output = worker.tick(); // then assertThat(output).isEqualTo( items.subList(0, rollValue)); }
  • 57.
    Еще аналогичные тесты: •Проверяем, что enqueue() добавляет в очередь • Проверяем, что tick() удаляет из очереди обработанные детали
  • 58.
    Тупой, но важныйтест: Во время Worker.tick() кубик бросается ровно один раз!
  • 59.
    Во время тренингов доводилось встречать такой код: public List<Item> tick() { final List<Item> output = new LinkedList<Item>(); for (int i = 0; i < dice.roll(); i++) { if (queue.isEmpty()) break; output.add(queue.poll()); } return output; }
  • 60.
    Всё работает, но распределение… P(1) = 1/6 = 0.1(6) P(2) = 5/18 = 0.2(7) P(3) = 5/18 = 0.2(7) P(4) = 5/27 = 0.(185) P(5) = 25/324  0.077 P(6) = 5/324  0.0154
  • 61.
    @Test public void shouldRollDiceOnlyOnceDuringTick(){ // given final Dice dice = createDiceStub(3); final Worker worker = new Worker(dice); final List<Item> items = Arrays.asList( new Item(), new Item(), new Item(), new Item()); worker.enqueue(items); // when worker.tick(); // then verify(dice, times(1)).roll(); }
  • 62.
    [TestMethod] public void ShouldRollDiceOnlyOnceDuringTick() { // given var diceMock = new Mock<Dice>(); diceMock.Setup(d => d.Roll()).Returns(3); var dice = diceMock.Object; var worker = new Worker(dice); var items = new[] { new Item(), new Item(), new Item(), new Item() }; worker.Enqueue(items); // when worker.Tick(); // then diceMock.Verify(d => d.Roll(), Times.Once()); }
  • 63.
    Это примитивный пример теста на поведение: мы проверили как взаимодействует наш объект с другим объектом
  • 64.
    На состояние На поведение mock Объект Объект
  • 65.
    Закрепим материал: • Проверим,что во время Worker.tick() вызывается Item.tick() для всех деталей, находящихся в очереди на начало tick()-а • Это можно проверить, не прибегая к mock-ам – через значение lifeTime(), но тогда мы тестируем два класса сразу, а не один в изоляции
  • 66.
    @Test public void shouldCallTickForAllItemsInQueue(){ // given final Dice dice = createDiceStub(1); final Worker worker = new Worker(dice); final Item firstItem = mock(Item.class); final Item secondItem = mock(Item.class); final List<Item> items = Arrays.asList( firstItem, secondItem); worker.enqueue(items); // when worker.tick(); // then verify(firstItem, times(1)).tick(); verify(secondItem, times(1)).tick(); }
  • 67.
    [TestMethod] public void ShouldCallTickForAllItemsInQueue() { // given var dice = CreateDiceStub(1); var worker = new Worker(dice); var firstItemMock = new Mock<Item>(); var secondItemMock = new Mock<Item>(); worker.Enqueue(new[] { firstItemMock.Object, secondItemMock.Object }); // when worker.Tick(); // then firstItemMock.Verify(i => i.Tick(), Times.Once()); secondItemMock.Verify(i => i.Tick(), Times.Once()); }
  • 69.
    Совет «по случаю» • Избегайте имен переменных item1, item2 и т.п. • Точно запутаетесь и опечатаетесь • Лучше говорящие имена • Или на худой конец: firstItem, secondItem и т.п.
  • 71.
  • 72.
    Conveyor tick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick() dice 1 Dice roll():int
  • 73.
  • 74.
    Тесты на состояние: •Можно придумать несколько тестовых сценариев (разрисовать на бумажке) o в стиле «Контрольный пример» из начала презентации • Проблемы: o Но как они помогут написать реализацию? o Какие тесты написать первыми, а какие потом? o Как быть уверенным, что протестированы все случаи и нюансы? (полнота покрытия) o Как эти тесты будут соотноситься со спецификацией? (test == executable specification)
  • 75.
    Напомним спецификацию: 1. То,что подается на вход конвейера, сражу же оказывается в очереди первого рабочего, т.е. до начала обработки им деталей 2. То, что обработал последний рабочий, является выходом конвейера за соответствующий цикл 3. Для всех остальных рабочих их результат работы попадает в очередь к следующему рабочему, но уже после того, как тот произвел обработку
  • 76.
    Min Max Кол- День Cycle Cycle во Time Time 1 0 - - N 2 0 - - 3 2 3 3 4 3 3 4 5 1 4 4 Cycle Time
  • 77.
    Тесты на поведение позволяют протестировать эту спецификацию один в один
  • 78.
    1. То, чтоподается на вход конвейера, сражу же оказывается в очереди первого рабочего, т.е. до начала обработки им деталей 2. То, что обработал последний рабочий, является выходом конвейера за соответствующий цикл 3. Для всех остальных рабочих их результат работы попадает в очередь к следующему рабочему, но уже после того, как тот произвел обработку
  • 79.
    worker1 worker2 1. enqueue( ) 2. tick conveyor Conveyor tick( ) Conveyor(Worker[*]) tick(Item[*]):Item[*]
  • 80.
    @Test public void shouldEnqueueInputToFirstWorkerBeforeProcessing(){ // given final List<Item> someInput = Arrays.asList( new Item(), new Item(), new Item()); final Worker firstWorker = mock(Worker.class); final Worker secondWorker = mock(Worker.class); final List<Worker> workers = Arrays.asList( firstWorker, secondWorker); final Conveyor conveyor = new Conveyor(workers); // when conveyor.tick(someInput); // then final InOrder order = inOrder(firstWorker); order.verify(firstWorker, times(1)).enqueue(someInput); order.verify(firstWorker, times(1)).tick(); }
  • 81.
    [TestMethod] public void ShouldEnqueueInputToFirstWorkerBeforeProcessing(){ // given var someInput = new[] { new Item(), new Item }; var callSequence = MoqSequence.Create(); var firstWorkerMock = new Mock<Worker>(); firstWorkerMock.Setup(w => w.Enqueue(someInput)) .InSequence(callSequence); firstWorkerMock.Setup(w => w.Tick()) .InSequence(callSequence); var firstWorker = firstWorkerMock.Object; var secondWorker = new Mock<Worker>().Object; var conveyor = new Conveyor(new[]{firstWorker, secondWorker }); // when conveyor.Tick(someInput); // then callSequence.Verify(); }
  • 82.
    Недостаток Moq: нет «встроенной»поддержки последовательностей вызовов Moq.Sequences.dll
  • 83.
    public static classMoqSequence { public interface ISequence { void Verify(); } public static ISequence Create() { return new Sequence(); } public static void InSequence(this ICallback mock, ISequence sequence) { var seq = (Sequence)sequence; var id = seq.GetNextCallId(); mock.Callback(() => seq.LogCall(id)); } private class Sequence : ISequence { public int GetNextCallId() { return nextCallId++; } public void LogCall(int id) { calls.Add(id); } public void Verify() { calls.Count.Should().Be(nextCallId, "it's expected count of calls in sequence"); for (var i = 0; i < calls.Count; i++) { calls[i].Should().Be(i, "wrong call sequence in position {0} ", i); } } private int nextCallId; private readonly IList<int> calls = new List<int>(); } }
  • 84.
    1. То, чтоподается на вход конвейера, сражу же оказывается в очереди первого рабочего, т.е. до начала обработки им деталей 2. То, что обработал последний рабочий, является выходом конвейера за соответствующий цикл 3. Для всех остальных рабочих их результат работы попадает в очередь к следующему рабочему, но уже после того, как тот произвел обработку
  • 85.
    @Test public void shouldReturnOutputOfLastWorker(){ // given final List<Item> someOutput = Arrays.asList( new Item(), new Item()); final Worker firstWorker = mock(Worker.class); final Worker secondWorker = mock(Worker.class); when(secondWorker.tick()).thenReturn(someOutput); final List<Worker> workers = Arrays.asList( firstWorker, secondWorker); final Conveyor conveyor = new Conveyor(workers); final List<Item> someInput = Arrays.asList(new Item()); // when final List<Item> output = conveyor.tick(someInput); // then assertThat(output).isEqualTo(someOutput); }
  • 86.
    Это можно былопроверить, создав реальных Worker-ов, «накормив» заранее второго нужными Item-ами, но такие тесты уже больше похожи на интеграционные
  • 87.
    Mock-и очень удобны, чтобы имитировать любое необходимое состояние стороннего объекта, не связываясь с длинной цепочкой вызовов, необходимой для приведения реального объекта в это состояние
  • 89.
    Fake Mock Stub, Dummy
  • 91.
    1. То, чтоподается на вход конвейера, сражу же оказывается в очереди первого рабочего, т.е. до начала обработки им деталей 2. То, что обработал последний рабочий, является выходом конвейера за соответствующий цикл 3. Для всех остальных рабочих их результат работы попадает в очередь к следующему рабочему, но уже после того, как тот произвел обработку
  • 92.
    conveyor firstWorker secondWorker thirdWorker tick() enqueue() tick()  tick()  enqueue() tick()  enqueue() 
  • 93.
    @Test public void shouldEnqueueOutputOfPreviousWorkerToTheNextAfterProcessing(){ // given final List<Item> outputOfFirstWorker = Arrays.asList( new Item(), new Item()); final List<Item> outputOfSecondWorker = Arrays.asList( new Item(), new Item(), new Item()); final Worker firstWorker = mock(Worker.class); when(firstWorker.tick()).thenReturn(outputOfFirstWorker); final Worker secondWorker = mock(Worker.class); when(secondWorker.tick()).thenReturn(outputOfSecondWorker); final Worker thirdWorker = mock(Worker.class); final List<Worker> workers = Arrays.asList( firstWorker, secondWorker, thirdWorker); final Conveyor conveyor = new Conveyor(workers); final List<Item> someInput = Arrays.asList(new Item());
  • 94.
    // when conveyor.tick(someInput); // then InOrder secondWorkerOrder = inOrder(secondWorker); secondWorkerOrder.verify(secondWorker).tick(); secondWorkerOrder.verify(secondWorker) .enqueue(outputOfFirstWorker); InOrder thirdWorkerOrder = inOrder(thirdWorker); thirdWorkerOrder.verify(thirdWorker).tick(); thirdWorkerOrder.verify(thirdWorker) .enqueue(outputOfSecondWorker); }
  • 95.
    Тест на поведение– это проверка, что код соответствует задуманной диаграмме последовательности
  • 96.
  • 97.
    public List<Item> tick(finalList<Item> input) { if (workers.isEmpty()) { return input; } final Worker firstWorker = workers.get(0); firstWorker.enqueue(input); List<Item> output = firstWorker.tick(); for (int i = 1; i < workers.size(); i++) { final Worker worker = workers.get(i); final List<Item> tmp = worker.tick(); worker.enqueue(output); output = tmp; } return output; }
  • 98.
    public virtual IList<Item>Tick(IList<Item> input) { if (workers.Count == 0) return input; var firstWorker = workers.First(); firstWorker.Enqueue(input); var lastOutput = firstWorker.Tick(); foreach (var worker in workers.Skip(1)) { var tmp = worker.Tick(); worker.Enqueue(lastOutput); lastOutput = tmp; } return lastOutput; }
  • 99.
  • 100.
    • Java o junit + mockito + fest o проект NetBeans 7.0 • .Net (C#) o ms unit + moq + fluentAssertions o проект VS 2010 • C++ o google test + google mock o проект VS 2010 • PHP o phpUnit o проект PhpStorm 3.0
  • 101.
    WARNING: В коде не выделены интерфейсы для Item, Dice и Worker только в целях «упрощения» примера. Выделение интерфейсов предпочтительнее перекрытия самих классов с функциональностью. «interface» Dice RandomDice roll():int
  • 102.
  • 103.
    Плюсы тестов наповедение • Просто писать (когда привыкнешь) o не нужно долго и мучительно приводить окружение в нужное состояние • Являются истинными unit-тестами o проверяют функционал класса в изоляции от всех остальных • Хорошо отражают спецификации и дают уверенность в хорошем покрытии кода o executable specification • Принуждают к модульному дизайну o SRP, LSP, DIP, ISP • Позволяют разрабатывать функционал сверху-вниз от сценариев использования o а не снизу вверх от данных
  • 104.
    1 Conveyor tick(Item[*]):Item[*] workers * 2 Worker 3 Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick() dice 1 4 Dice roll():int
  • 105.
    Минусы тестов наповедение • Чтобы ими овладеть, требуется ментальный сдвиг • Проверяют, что код работает так, как вы ожидаете, но это не значит, что он работает правильно o этот недостаток легко снимается небольшим количеством интеграционных тестов • Не весь функционал можно так протестировать • Тесты хрупкие o изменение в реализации ломает тесты • Требуют выделения интерфейсов или виртуальности методов o Обычно не проблема. А если проблема, то используйте «быстрые mock-и» на C++ templates
  • 106.
    Mock Hell • Чтобыего избежать, очень важно соблюдать ISP o Широко используемыми могут быть только стабильные интерфейсы
  • 107.
  • 108.
  • 111.
    Пример by Miško Hevery http://bit.ly/92Ozrz http://goo.gl/V0aWx
  • 112.
    public class InboxSyncer{ private static final InboxSyncer instance = new InboxSyncer(Config.get()); public static getInstance() { return instance; } private final Certificate cert; private final String username; private final String password; private long lastSync = -1; private InboxSyncer(Config config) { this.cert = DESReader.read(config.get(“cert.path”)); User user = config.getUser(); this.username = user.getUsername(); this.password = user.getPassword(); }
  • 113.
    public sync() { long syncFrom = lastSync; if (syncFrom == -1) { syncFrom = new Date().getTime() - Defaults.INBOX_SYNC_AMOUNT; } Inbox inbox = Inbox.get(username); POPConnector pop = new POPConnector(cert, username, password); pop.connect(); try { Iterator<Messages> messages = pop.messagesIterator(); Message message; while ( messages.hasNext() && (message = messages.next()).getTime() > syncFrom ) { if (Defaults.FILTER_SPAM ? MessageFilter.spam(message) : MessageFilter.addressedToMe(message, username)) { this.lastSync = Math.max(this.lastSync, message.getTime()); if (!inbox.contains(message.getId()) { inbox.add(message); } } } } finally { pop.disconnect(); } } }
  • 114.
    Если писать на«это» тест в лоб
  • 115.
    public class InboxSyncerTestextends TestCase { PopServer server; Certificate cert; static InboxSyncer syncer; public setUp() { if (syncer == null) { Config config = Config.get(); User user = new User(); user.setUsername(“[email protected]”); user.setPassword(“myTestPassword”); config.setUser(user); cert = DESCert.generateCert(); File tempCert = File.creteTempFile(“temp”, “.cert”); FileOutputStream out = new FileOutputStream(tempCert); out.write(cert); out.close(); config.set(“cert.path”, tempCert.toString()); syncer = InboxSyncer.get(); } // Reset the state of Global Objects Inbox inbox = Inbox.get(“[email protected]”); Iterator<Message> messages = inbox.getMessages(); while(messages.hasNext()) { messages.remove(); } Reflection.setPrivateField(syncer, “lastSync”, -1); POPServer server = new POPServer(cert); server.start(); MessageQueue queue = server.getMessageQueueForUser(“[email protected]”); queue.clear(); }
  • 116.
    public tearDown() { server.stop(); } public testSync() { Defaults.FILTER_SPAM = true; MessageQueue queue = server.getMessageQueueForUser(“[email protected]”); Date time = new Date(); Message message = new Message(“from”, “to”, “subject”, “message”, time); queue.addMessage(message); syncer.sync(); Inbox inbox = Inbox.get(“[email protected]”); assertTrue(inbox.contains(message.getId()); assertEquals(1, inbox.getMessages().size()); } }
  • 117.
    Что не такс кодом?
  • 118.
    Глобальный контекст (Singleton) • Нарушение закона Деметры (Law of Demeter) • Использование текущего системного времени • Самостоятельное создание объектов • Отсутствие Dependency Injection
  • 119.
    public class InboxSyncer{ private final POPConnector pop; private final Inbox inbox; private final Filter filter; private final Date lastSync; private final long defaultPastSync; private InboxSyncer(POPConnector pop, Inbox inbox, Filter filter, Date lastSync, long defaultPastSync) { this.pop = pop; this.inbox = inbox; this.filter = filter; this.lastSync = lastSync; this.defaultPastSync = defaultPastSync; }
  • 120.
    public sync(Date now){ Date syncFrom = lastSync; if (syncFrom == null) { syncFrom = new Date(now.getTime() - defaultPastSync); } pop.connect(); try { Iterator<Messages> messages = pop.messagesIterator(); Message message; while ( messages.hasNext() && (message = messages.next()).getTime() > syncFrom.getTime() ) { if (filter.apply(message)) { this.lastSync = new Date(Math.max( this.lastSync.getTime(), message.getTime())); if (!inbox.contains(message.getId()) { inbox.add(message); } } } } finally { pop.disconnect(); } } }
  • 121.
  • 122.
    сlass InboxSyncerSpec extendsTestCase { Date longEgo = new Date(1); Date past = new Date(2); Date now = new Date(3); Date future = new Date(4); POPConnector pop = new MockPOPConnector(); Inbox inbox = new Inbox(); Filter filter = new NoopFilter(); Message longEgoMsg = new Message(“from”, “to”, “subject”, “msg”, longEgo); Message pastMsg = new Message(“me”, “you”, “hello”, “world”, past); long noDefaultSync = -1; public @Test itShouldSyncMessagesFromPopSinceLastSync() { pop.addMessage(longEgoMsg); pop.addMessage(pastMsg); new InboxSyncer(pop, inbox, filter, longEgo, noDefaultSync).sync(now); assertThat(inbox.getMessages(), is(equalTo(pastMsg))); assertThat(pop.isClosed(), is(true)); }
  • 123.
    public @Test itShouldCloseConnectionEvenWhenExceptionThrown(){ Exception exception = new Exception(); Filter filter = new ExceptionThrowingFilter(exception); InboxSyncer syncer = new InboxSyncer(pop, null, filter, null, noDefaultSync); try { syncer.sync(now); fail(“Exception expected!”); } catch (Exception e) { assertThat(e, is(exception)); assertThat(pop.isClosed(), is(true)); } } public @Test itShouldSyncMessagesOnlyWhenNotAlreadyInInbox() { pop.addMessage(pastMsg); inbox.add(pastMsg); new InboxSyncer(pop, inbox, filter, longEgo, noDefaultSync).sync(now); assertThat(inbox.getMessages(), is(equalTo(pastMsg))); } public @Test itShouldIgnoreMessagesOlderThenLastSync() {} public @Test itShouldIgnoreMessagesFailingFilter() {} public @Test itShouldDefaultToDefaultTimeWhenNeverSynced() {} }
  • 124.
  • 125.
  • 129.
    DCI Data, context andinteraction http://architects.dzone.com/videos/dci-architecture-trygve http://www.artima.com/articles/dci_vision.html
  • 130.
  • 131.
    Роль декомпозиции функционалана отдельные классы при следовании TDD
  • 132.
  • 133.
    Тесты на поведениепротив тестов на состояние
  • 134.
    Спасибо за внимание! Вопросы? @bibigine [email protected] http://tinyurl.com/bibigine http://www.slideshare.net/bibigine