Mockist vs Classicist
Upcoming SlideShare
Loading in...5
×
 

Mockist vs Classicist

on

  • 2,279 views

Presentation for the 1st XPDays Ukrain conference.

Presentation for the 1st XPDays Ukrain conference.

Statistics

Views

Total Views
2,279
Views on SlideShare
1,024
Embed Views
1,255

Actions

Likes
4
Downloads
19
Comments
0

3 Embeds 1,255

http://mcgray.com.ua 742
http://xpdays.com.ua 512
http://a0.twimg.com 1

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

CC Attribution-ShareAlike LicenseCC Attribution-ShareAlike License

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Mockist vs Classicist Mockist vs Classicist Presentation Transcript

  • Тесты на поведение супротивтестов на состояние Бибичев Андрей декабрь 2011
  • @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 можно использовать для обучения и проверки других
  • Min Max Кол- День Cycle Cycle во Time Time 1 0 - - N 2 0 - - 3 2 3 3 4 3 3 4 5 1 4 4Cycle Time
  • Контрольный пример: Min Max Кол- День Cycle Cycle во Time Time 1 0 - - 2 0 - - 5 3 0 - - 4 0 - - 5 1 5 5
  • Conveyortick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick()
  • Conveyor ItemWorker
  • Conveyortick(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
  • @Testpublic 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++; }}
  • Conveyortick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick()
  • Workerqueue
  • Рабочий ничего не обрабатывает, если нет деталей 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(); }
  • Если во время обработки на кубике выпало значениебольшее количества деталей в очереди, то рабочий обрабатывает все детали в очереди (и больше ничего)
  • Conveyortick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick() dice 1 Dice roll():int
  • Dice Workerqueue
  • Dependency Injection (DI) через конструктор Worker Worker(Dice) enqueue(Item[*]) tick():Item[*]
  • @Testpublic 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);}
  • 4enqueue 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 деталей из очереди
  • @Testpublic 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.077P(6) = 5/324  0.0154
  • @Testpublic 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(), но тогда мы тестируем два класса сразу, а не один в изоляции
  • @Testpublic 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 и т.п.
  • Переходим к самому интересному
  • Conveyortick(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 4Cycle Time
  • Тесты на поведение позволяютпротестировать эту спецификацию один в один
  • 1. То, что подается на вход конвейера, сражу же оказывается в очереди первого рабочего, т.е. до начала обработки им деталей2. То, что обработал последний рабочий, является выходом конвейера за соответствующий цикл3. Для всех остальных рабочих их результат работы попадает в очередь к следующему рабочему, но уже после того, как тот произвел обработку
  • worker1 worker21. enqueue( )2. tick conveyor Conveyor tick( ) Conveyor(Worker[*]) tick(Item[*]):Item[*]
  • @Testpublic 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, "its 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. Для всех остальных рабочих их результат работы попадает в очередь к следующему рабочему, но уже после того, как тот произвел обработку
  • @Testpublic 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 thirdWorkertick() enqueue() tick()  tick()  enqueue() tick()  enqueue() 
  • @Test public voidshouldEnqueueOutputOfPreviousWorkerToTheNextAfterProcessing() { // 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;}
  • Полный код
  • ZIP-архивhttp://tinyurl.com/tdd-demo-for-xpdays 10 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 Conveyortick(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. ClassicistMockist + Classicist
  • Спасибо за внимание! Вопросы? @bibigine bibigine@gmail.com http://tinyurl.com/bibigine http://www.slideshare.net/bibigine