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

  • 2,576 views
Uploaded on

Слайды с мастер-класса "Быстрое введение в TDD от А до Я" с конференции AgileDays'12 (март 2012, Москва)

Слайды с мастер-класса "Быстрое введение в TDD от А до Я" с конференции AgileDays'12 (март 2012, Москва)

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
2,576
On Slideshare
0
From Embeds
0
Number of Embeds
2

Actions

Shares
Downloads
44
Comments
0
Likes
4

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. Быстроевведениев TDDот А до ЯБибичев Андреймарт 2012
  • 2. @bibigine Андрей Бибичев• E-mail: bibigine@gmail.com• Twitter: @bibigine• Profile: http://tinyurl.com/bibigine• Slideshare: http://www.slideshare.net/bibigine
  • 3. Для кого и зачем?• Beginner o хорошая точка входа• Intermediate o поможет лучше всё структурировать в голове и объяснять коллегам• Advanced o можно использовать для обучения и проверки других
  • 4. Введение:Data-Driven тесты А
  • 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. TDD:классический пример Б
  • 8. http://www.objectmentor.com/resources/articles/xpepisode.htmhttp://wiki.agiledev.ru/doku.php?id=tdd:bowlinghttp://www.slideshare.net/stewshack/bowling-game-kata-c
  • 9. BowlingGameroll(pins: int)getScore(): intgetFrameScore(indx: int): intgetCurrentFrame(): int
  • 10. Тесты1. На 0 (ничего не сбито)2. Простой фрейм3. Очки по фреймам4. Номер текущего фрейма5. Spare6. Strike7. Spare в 10-ом фрейме8. Strike в 10-ом фрейме
  • 11. Цикл разработки в TDD Написание неработающего теста для новой функциональности RED REFACTOR GREEN Рефакторим код, чтобы Пишем ровно столько кода,тесты продолжили проходить, чтобы тест прошел а код стал чистым
  • 12. Традиционный цикл разработки Пишем сразу заметный кусок функциональности CODING DEBUGGING COMPILING Отлаживаем код, Добиваемся ловим и исправляем компилируемости поверхностные баги
  • 13. TDD:состояние vs поведение В-Э
  • 14. Min Max Кол- День Cycle Cycle во Time Time 1 0 - - N 2 0 - - 3 2 3 3 4 3 3 4 5 1 4 4Cycle Time
  • 15. Контрольный пример: Min Max Кол- День Cycle Cycle во Time Time 1 0 - - 2 0 - - 5 3 0 - - 4 0 - - 5 1 5 5
  • 16. Диаграмма классов
  • 17. Conveyortick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick()
  • 18. Conveyor ItemWorker
  • 19. Conveyortick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick()
  • 20. У вновь созданной деталивремя жизни должно равняться нулю дано: новая деталь, когда: запрашиваем у нее время жизни, тогда: получаем в результате 0
  • 21. 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
  • 22. 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
  • 23. Это «вырожденный» тест на состояние
  • 24. public class Item { public int lifeTime() { return 0; }}public class Item{ public int LifeTime { get; }}
  • 25. Дано: арбуз + гиря 1 кг = гиря 6 кг арбуз = ?Решение: x+1=6 x=6–1=5Ответ: 5 кг
  • 26. vs.Test… Should…{ { // Arrange // GIVEN … … // Action // WHEN … … // Assertion // THEN … …} }
  • 27. Март 2006Журнал «Better Software»Статья «Introducing BDD» Dan North
  • 28. Альтернатива ROY OSHEROVE
  • 29. ИмяМетода_Условия_Результат[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
  • 30. Критерий хорошо оформленного теста:• Содержательное название• Короткое тело (max = 20-30 строк)• По шаблону AAA или GIVEN-WHEN-THEN• Без циклов• Без ветвлений (if-ов и case-ов)• Должен легко читаться (literate programming)
  • 31. Оповещение о том, что прошел такт конвейера должно увеличиватьзначение времени жизни на один дано: новая деталь,когда: оповещаем ее о такте конвейера, тогда: время жизни становится 1
  • 32. @Testpublic void shouldIncrementLifeTimeDuringTick() { // given final Item item = new Item(); // when item.tick(); // then assertThat(item.lifeTime()).isEqualTo(1);}
  • 33. [TestMethod]public void ShouldIncrementLifeTimeDuringTick(){ // given var item = new Item(); // when item.Tick(); // then item.LifeTime.Should().Be(1);}
  • 34. Это примитивный пример теста на состояние
  • 35. 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++; }}
  • 36. Conveyortick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick()
  • 37. Workerqueue
  • 38. Рабочий ничего не обрабатывает, если нет деталей x У вновь созданного рабочего входная очередь деталей пуста
  • 39. public class WorkerTest { @Test public void shouldReturnNothingIfNothingToDo() { // given final Worker worker = new Worker(); // when final List<Item> output = worker.tick(); // then assertThat(output).isEmpty();}
  • 40. [TestClass]public class WorkerTest{ [TestMethod] public void ShouldReturnNothingIfNothingToDo() { // given var worker = new Worker(); // when var output = worker.Tick(); // then output.Should().BeEmpty(); }
  • 41. Если во время обработки на кубике выпало значениебольшее количества деталей в очереди, то рабочий обрабатывает все детали в очереди (и больше ничего)
  • 42. Conveyortick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick() dice 1 Dice roll():int
  • 43. Dice Workerqueue
  • 44. Dependency Injection (DI) через конструктор Worker Worker(Dice) enqueue(Item[*]) tick():Item[*]
  • 45. @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);}
  • 46. [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);}
  • 47. 4enqueue tick Dice roll():int DiceStub roll():int
  • 48. 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;}
  • 49. Хотя мы и воспользовались mock-объектом,это всё равно, по большому счету, тест на состояние
  • 50. Если во время обработкина кубике выпало значение N, меньше количества деталей в очереди, то обрабатывается толькопервые N деталей из очереди
  • 51. @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));}
  • 52. Еще аналогичные тесты:• Проверяем, что enqueue() добавляет в очередь• Проверяем, что tick() удаляет из очереди обработанные детали
  • 53. Тупой, но важный тест: Во время Worker.tick() кубик бросается ровно один раз!
  • 54. Во время тренингов доводилось встречать такой код: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;}
  • 55. Всё работает, но распределение…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
  • 56. @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();}
  • 57. [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());}
  • 58. Это примитивный пример теста на поведение:мы проверили как взаимодействует наш объект с другим объектом
  • 59. На состояние На поведение mock Объект Объект
  • 60. Закрепим материал:• Проверим, что во время Worker.tick() вызывается Item.tick() для всех деталей, находящихся в очереди на начало tick()-а• Это можно проверить, не прибегая к mock-ам – через значение lifeTime(), но тогда мы тестируем два класса сразу, а не один в изоляции
  • 61. @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();}
  • 62. [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());}
  • 63. Совет «по случаю»• Избегайте имен переменных item1, item2 и т.п.• Точно запутаетесь и опечатаетесь• Лучше говорящие имена• Или на худой конец: firstItem, secondItem и т.п.
  • 64. Переходим к самому интересному
  • 65. Conveyortick(Item[*]):Item[*] workers * Worker Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick() dice 1 Dice roll():int
  • 66. Как тестировать?
  • 67. Тесты на состояние:• Можно придумать несколько тестовых сценариев (разрисовать на бумажке) o в стиле «Контрольный пример» из начала презентации• Проблемы: o Но как они помогут написать реализацию? o Какие тесты написать первыми, а какие потом? o Как быть уверенным, что протестированы все случаи и нюансы? (полнота покрытия) o Как эти тесты будут соотноситься со спецификацией? (test == executable specification)
  • 68. Напомним спецификацию:1. То, что подается на вход конвейера, сражу же оказывается в очереди первого рабочего, т.е. до начала обработки им деталей2. То, что обработал последний рабочий, является выходом конвейера за соответствующий цикл3. Для всех остальных рабочих их результат работы попадает в очередь к следующему рабочему, но уже после того, как тот произвел обработку
  • 69. Min Max Кол- День Cycle Cycle во Time Time 1 0 - - N 2 0 - - 3 2 3 3 4 3 3 4 5 1 4 4Cycle Time
  • 70. Тесты на поведение позволяютпротестировать эту спецификацию один в один
  • 71. 1. То, что подается на вход конвейера, сражу же оказывается в очереди первого рабочего, т.е. до начала обработки им деталей2. То, что обработал последний рабочий, является выходом конвейера за соответствующий цикл3. Для всех остальных рабочих их результат работы попадает в очередь к следующему рабочему, но уже после того, как тот произвел обработку
  • 72. worker1 worker21. enqueue( )2. tick conveyor Conveyor tick( ) Conveyor(Worker[*]) tick(Item[*]):Item[*]
  • 73. @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();}
  • 74. [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();}
  • 75. Недостаток Moq:нет «встроенной» поддержкипоследовательностей вызовов Moq.Sequences.dll
  • 76. 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>(); }}
  • 77. 1. То, что подается на вход конвейера, сражу же оказывается в очереди первого рабочего, т.е. до начала обработки им деталей2. То, что обработал последний рабочий, является выходом конвейера за соответствующий цикл3. Для всех остальных рабочих их результат работы попадает в очередь к следующему рабочему, но уже после того, как тот произвел обработку
  • 78. @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);}
  • 79. Это можно было проверить, создав реальных Worker-ов,«накормив» заранее второго нужными Item-ами, но такие тесты уже больше похожи на интеграционные
  • 80. Mock-и очень удобны, чтобы имитировать любое необходимое состояние стороннего объекта, не связываясь с длиннойцепочкой вызовов, необходимой для приведения реального объекта в это состояние
  • 81. Fake Mock Stub, Dummy
  • 82. 1. То, что подается на вход конвейера, сражу же оказывается в очереди первого рабочего, т.е. до начала обработки им деталей2. То, что обработал последний рабочий, является выходом конвейера за соответствующий цикл3. Для всех остальных рабочих их результат работы попадает в очередь к следующему рабочему, но уже после того, как тот произвел обработку
  • 83. conveyor firstWorker secondWorker thirdWorkertick() enqueue() tick()  tick()  enqueue() tick()  enqueue() 
  • 84. @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());
  • 85. // 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);}
  • 86. Тест на поведение – это проверка,что код соответствует задуманной диаграмме последовательности
  • 87. Реализация
  • 88. 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;}
  • 89. 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;}
  • 90. Полный кодhttp://tinyurl.com/tdd-demo-for-agiledays 17 Mb
  • 91. • 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
  • 92. WARNING: В коде не выделены интерфейсы для Item, Dice и Workerтолько в целях «упрощения» примера. Выделение интерфейсовпредпочтительнее перекрытия самих классов с функциональностью. «interface» Dice RandomDice roll():int
  • 93. Итого
  • 94. Плюсы тестов на поведение• Просто писать (когда привыкнешь) o не нужно долго и мучительно приводить окружение в нужное состояние• Являются истинными unit-тестами o проверяют функционал класса в изоляции от всех остальных• Хорошо отражают спецификации и дают уверенность в хорошем покрытии кода o executable specification• Принуждают к модульному дизайну o SRP, LSP, DIP, ISP• Позволяют разрабатывать функционал сверху-вниз от сценариев использования o а не снизу вверх от данных
  • 95. 1 Conveyortick(Item[*]):Item[*] workers * 2 Worker 3 Item queue enqueue(Item[*]) * lifeTime():int tick():Item[*] tick() dice 1 4 Dice roll():int
  • 96. Минусы тестов на поведение• Чтобы ими овладеть, требуется ментальный сдвиг• Проверяют, что код работает так, как вы ожидаете, но это не значит, что он работает правильно o этот недостаток легко снимается небольшим количеством интеграционных тестов• Не весь функционал можно так протестировать• Тесты хрупкие o изменение в реализации ломает тесты• Требуют выделения интерфейсов или виртуальности методов o Обычно не проблема. А если проблема, то используйте «быстрые mock-и» на C++ templates
  • 97. Mock Hell• Чтобы его избежать, очень важно соблюдать ISP o Широко используемыми могут быть только стабильные интерфейсы
  • 98. Mockist vs. ClassicistMockist + Classicist
  • 99. Legacy-код Ю
  • 100. Примерby Miško Hevery http://bit.ly/92Ozrz http://goo.gl/V0aWx
  • 101. 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(); }
  • 102. 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(); } }}
  • 103. Если писать на «это» тест в лоб
  • 104. 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(); }
  • 105. 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()); }}
  • 106. Что не так с кодом?
  • 107. • Глобальный контекст (Singleton)• Нарушение закона Деметры (Law of Demeter)• Использование текущего системного времени• Самостоятельное создание объектов• Отсутствие Dependency Injection
  • 108. 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; }
  • 109. 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(); } }}
  • 110. Теперь тесты:
  • 111. с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)); }
  • 112. 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() {} }
  • 113. Тестопригодная архитектура Я
  • 114. http://www.youtube.com/watch?v=WpkDN78P884
  • 115. DCIData, context and interaction http://architects.dzone.com/videos/dci-architecture-trygve http://www.artima.com/articles/dci_vision.html
  • 116. Смежныевыступления bonus
  • 117. Роль декомпозиции функционала наотдельные классы при следовании TDD
  • 118. Архитектура в Agile:слабая связность кода
  • 119. Тесты на поведение против тестов на состояние
  • 120. Спасибо за внимание! Вопросы? @bibigine bibigine@gmail.com http://tinyurl.com/bibigine http://www.slideshare.net/bibigine