3. Для кого и зачем?
• Beginner
o хорошая точка входа
• Intermediate
o поможет лучше всё структурировать в голове и
объяснять коллегам
• Advanced
o можно использовать для обучения и проверки
других
4. 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
5. Контрольный пример:
Min Max
Кол-
День Cycle Cycle
во
Time Time
1 0 - -
2 0 - -
5 3 0 - -
4 0 - -
5 1 5 5
9. У вновь созданной детали
время жизни должно равняться нулю
дано: новая деталь,
когда: запрашиваем у нее время жизни,
тогда: получаем в результате 0
10. 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
11. 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
20. Критерий
хорошо оформленного теста:
• Содержательное название
• Короткое тело (max = 20-30 строк)
• По шаблону AAA или GIVEN-WHEN-THEN
• Без циклов
• Без ветвлений (if-ов и case-ов)
• Должен легко читаться
(literate programming)
21.
22. Оповещение о том, что
прошел такт конвейера
должно увеличивать
значение времени жизни на один
дано: новая деталь,
когда: оповещаем ее о такте конвейера,
тогда: время жизни становится 1
26. 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++; }
}
29. Рабочий ничего не обрабатывает,
если нет деталей
x
У вновь созданного рабочего
входная очередь деталей пуста
30. public class WorkerTest {
@Test
public void shouldReturnNothingIfNothingToDo() {
// given
final Worker worker = new Worker();
// when
final List<Item> output = worker.tick();
// then
assertThat(output).isEmpty();
}
31. [TestClass]
public class WorkerTest
{
[TestMethod]
public void ShouldReturnNothingIfNothingToDo()
{
// given
var worker = new Worker();
// when
var output = worker.Tick();
// then
output.Should().BeEmpty();
}
32. Если во время обработки
на кубике выпало значение
большее количества деталей
в очереди,
то рабочий обрабатывает
все детали в очереди
(и больше ничего)
36. @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);
}
37. [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);
}
39. 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;
}
40.
41. Хотя мы и воспользовались
mock-объектом,
это всё равно, по большому счету,
тест на состояние
42. Если во время обработки
на кубике выпало значение N,
меньше количества деталей
в очереди,
то обрабатывается только
первые N деталей из очереди
43. @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));
}
44. Еще аналогичные тесты:
• Проверяем, что enqueue() добавляет в
очередь
• Проверяем, что tick() удаляет из очереди
обработанные детали
45. Тупой, но важный тест:
Во время Worker.tick()
кубик бросается
ровно один раз!
46. Во время тренингов
доводилось встречать такой код:
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;
}
48. @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();
}
49. [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());
}
50. Это примитивный пример
теста на поведение:
мы проверили как взаимодействует
наш объект с другим объектом
52. Закрепим материал:
• Проверим, что во время Worker.tick()
вызывается Item.tick() для всех деталей,
находящихся в очереди на начало
tick()-а
• Это можно проверить, не прибегая к
mock-ам – через значение lifeTime(), но
тогда мы тестируем два класса сразу, а
не один в изоляции
53. @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();
}
54. [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());
}
55.
56. Совет «по случаю»
• Избегайте имен переменных item1, item2 и т.п.
• Точно запутаетесь и опечатаетесь
• Лучше говорящие имена
• Или на худой конец: firstItem, secondItem и т.п.
61. Тесты на состояние:
• Можно придумать несколько тестовых
сценариев (разрисовать на бумажке)
o в стиле «Контрольный пример» из начала презентации
• Проблемы:
o Но как они помогут написать реализацию?
o Какие тесты написать первыми, а какие потом?
o Как быть уверенным, что протестированы все случаи и
нюансы? (полнота покрытия)
o Как эти тесты будут соотноситься со спецификацией?
(test == executable specification)
62. Напомним спецификацию:
1. То, что подается на вход конвейера, сражу же
оказывается в очереди первого рабочего, т.е. до
начала обработки им деталей
2. То, что обработал последний рабочий, является
выходом конвейера за соответствующий цикл
3. Для всех остальных рабочих их результат работы
попадает в очередь к следующему рабочему,
но уже после того, как тот произвел обработку
63. 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
64. Тесты на поведение
позволяют
протестировать эту спецификацию
один в один
65. 1. То, что подается на вход конвейера, сражу же
оказывается в очереди первого рабочего, т.е. до
начала обработки им деталей
2. То, что обработал последний рабочий, является
выходом конвейера за соответствующий цикл
3. Для всех остальных рабочих их результат работы
попадает в очередь к следующему рабочему,
но уже после того, как тот произвел обработку
67. @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();
}
68. [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();
}
70. 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>();
}
}
71. 1. То, что подается на вход конвейера, сражу же
оказывается в очереди первого рабочего, т.е. до
начала обработки им деталей
2. То, что обработал последний рабочий, является
выходом конвейера за соответствующий цикл
3. Для всех остальных рабочих их результат работы
попадает в очередь к следующему рабочему,
но уже после того, как тот произвел обработку
72. @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);
}
73. Это можно было проверить,
создав реальных Worker-ов,
«накормив» заранее второго нужными
Item-ами,
но такие тесты уже больше похожи
на интеграционные
74. Mock-и очень удобны,
чтобы имитировать любое
необходимое состояние
стороннего объекта,
не связываясь с длинной
цепочкой вызовов, необходимой
для приведения реального
объекта в это состояние
78. 1. То, что подается на вход конвейера, сражу же
оказывается в очереди первого рабочего, т.е. до
начала обработки им деталей
2. То, что обработал последний рабочий, является
выходом конвейера за соответствующий цикл
3. Для всех остальных рабочих их результат работы
попадает в очередь к следующему рабочему,
но уже после того, как тот произвел обработку
80. @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());
88. • 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
89. WARNING:
В коде не выделены
интерфейсы для Item, Dice и Worker
только в целях «упрощения» примера.
Выделение интерфейсов
предпочтительнее перекрытия самих
классов с функциональностью.
«interface»
Dice RandomDice
roll():int
91. Плюсы тестов на поведение
• Просто писать (когда привыкнешь)
o не нужно долго и мучительно приводить окружение в нужное
состояние
• Являются истинными unit-тестами
o проверяют функционал класса в изоляции от всех остальных
• Хорошо отражают спецификации и дают
уверенность в хорошем покрытии кода
o executable specification
• Принуждают к модульному дизайну
o SRP, LSP, DIP, ISP
• Позволяют разрабатывать функционал
сверху-вниз от сценариев использования
o а не снизу вверх от данных
93. Минусы тестов на поведение
• Чтобы ими овладеть, требуется ментальный
сдвиг
• Проверяют, что код работает так, как вы
ожидаете, но это не значит, что он работает
правильно
o этот недостаток легко снимается небольшим количеством
интеграционных тестов
• Не весь функционал можно так
протестировать
• Тесты хрупкие
o изменение в реализации ломает тесты
• Требуют выделения интерфейсов или
виртуальности методов
o Обычно не проблема. А если проблема, то используйте «быстрые
mock-и» на C++ templates
94. Mock Hell
• Чтобы его избежать,
очень важно соблюдать ISP
o Широко используемыми могут быть только
стабильные интерфейсы