Testing & TDD

Preview:

Citation preview

TESTING ~

TDD

Kalinichev.net !

TDD? ЧТО И ОТКУДА?

• Пошло из XP: test-first

TDD? ЧТО И ОТКУДА?

• Пошло из XP: test-first

• Реже используешь отладчик

TDD? ЧТО И ОТКУДА?

• Пошло из XP: test-first

• Реже используешь отладчик

• Дизайн еще до реализации

TDD? ЧТО И ОТКУДА?

• Пошло из XP: test-first

• Реже используешь отладчик

• Дизайн еще до реализации

• Код менее связанный

TDD? ЧТО И ОТКУДА?

• Пошло из XP: test-first

• Реже используешь отладчик

• Дизайн еще до реализации

• Код менее связанный

• Нет сложной инициализации

TDD? ЧТО И ОТКУДА?

• Пошло из XP: test-first

• Реже используешь отладчик

• Дизайн еще до реализации

• Код менее связанный

• Нет сложной инициализации

• Интерфейсы четкие и небольшие

TDD? ЧТО И ОТКУДА?• Пошло из XP: test-first

• Реже используешь отладчик

• Дизайн еще до реализации

• Код менее связанный

• Нет сложной инициализации

• Интерфейсы четкие и небольшие

• Способствует модульному мышлению

!

TDD? ЧТО И ОТКУДА?

• Нужно привыкнуть

TDD? ЧТО И ОТКУДА?

• Нужно привыкнуть

• На старте может потребовать больше времени

TDD? ЧТО И ОТКУДА?

• Нужно привыкнуть

• На старте может потребовать больше времени

•Может возникнуть ложное чувство безопасности

TDD? ЧТО И ОТКУДА?

• Нужно привыкнуть

• На старте может потребовать больше времени

•Может возникнуть ложное чувство безопасности

• Плохо написанный тест - источник накладных расходов!

ПЛАН

• Что не так?

• Как хочется?

• Примеры

• Примеры

• Примеры

• Практика

ЧТО ВЫ МОЖЕТЕ СКАЗАТЬ О ТЕСТАХ?

Я ненавижу свои тесты

Я ненавижу свои тесты ... да и чужие

ПОЧЕМУ?

•Медленные

•Медленные

•Хрупкие

•Медленные

•Хрупкие

•Что еще хуже - Random’но хрупкие

•Медленные

•Хрупкие

•Что еще хуже - Random’но хрупкие

•Дорогие по деньгам

•Медленные

•Хрупкие

•Что еще хуже - Random’но хрупкие

•Дорогие по деньгам

•Что еще хуже - по времени

•Медленные

•Хрупкие

•Что еще хуже - Random’но хрупкие

•Дорогие по деньгам

•Что еще хуже - по времени

•Хрен поймешь - что тестируют?

•Медленные

•Хрупкие

•Что еще хуже - Random’но хрупкие

•Дорогие по деньгам

•Что еще хуже - по времени

•Хрен поймешь - что тестируют?

•Писать и разбираться в них - одна боль и страдание

ТАК НЕ ДОЛЖНО БЫТЬ!

Давайте удалим такие тесты

Давайте удалим такие тесты- Что все?

ЦЕЛЬ

UNIT ТЕСТЫ ДОЖНЫ БЫТЬ

UNIT ТЕСТЫ ДОЖНЫ БЫТЬ

•Полными

UNIT ТЕСТЫ ДОЖНЫ БЫТЬ

•Полными

•Стабильными

UNIT ТЕСТЫ ДОЖНЫ БЫТЬ

•Полными

•Стабильными

•Быстрыми

UNIT ТЕСТЫ ДОЖНЫ БЫТЬ

•Полными

•Стабильными

•Быстрыми

•Короткими

КАК ДЕЛА В КОДЕ НАШИХ ПРОЕКТОВ?

КАК ДЕЛА В КОДЕ НАШИХ ПРОЕКТОВ?

Давайте подумаем про один какой-нибудь объект...

ОБЪЕКТ

•Получает что-то от кого-то

•Отправляет что-то кому-то

•Внутри сам себе покоя не дает

ВСЕ ЭТО НЕКИЕ СООБЩЕНИЯ

СООБЩЕНИЯ МОЖНО РАЗБИТЬ НА ДВА ТИПА

Запросы: что-то возвращают и ничего не меняют

Запросы: что-то возвращают и ничего не меняют

Команды: ничего не возвращают и что-то меняют

Запросы: что-то возвращают и ничего не меняют

Команды: ничего не возвращают и что-то меняют

Избавляйтесь от запросов, которые ведут себя как команды

i

ЧТО И КАК ТЕСТИРОВАТЬ

Запросы Команды

Входящие

Самому себе

Исходящие

ВХОДЯЩИЕ СООБЩЕНИЯ

ВХОДЯЩИЕ ЗАПРОСЫ private class Aggregator {! private readonly int[] _args;! public Aggregator(params int[] args) {! _args = args;! }! public int Sum {! get { return _args.Sum(); }! }! }!!

ВХОДЯЩИЕ ЗАПРОСЫ private class Aggregator {! private readonly int[] _args;! public Aggregator(params int[] args) {! _args = args;! }! public int Sum {! get { return _args.Sum(); }! }! }!! [TestFixture]! public class AggregatorTest {! [Test]! public void Sum() {! var a = new Aggregator(1, 2, 3);! Assert.AreEqual(6, a.Sum);! }! }!

ВХОДЯЩИЕ ЗАПРОСЫ

Правило 1 !

Тестируем входящие запросы по тому что они возвращают

R

ВХОДЯЩИЕ ЗАПРОСЫ! private class MyAverage {! public const int MAGIC_COEF = 2;! private readonly int[] _args;! public MyAverage(params int[] args) {! _args = args;! }! public int Calc() {! return new Aggregator(ModifyArgs).Sum / _args.Length;! }! private int[] ModifyArgs {! get { return _args.Select(a => a * MAGIC_COEF).ToArray(); }! }! }!!

ВХОДЯЩИЕ ЗАПРОСЫ! private class MyAverage {! public const int MAGIC_COEF = 2;! private readonly int[] _args;! public MyAverage(params int[] args) {! _args = args;! }! public int Calc() {! return new Aggregator(ModifyArgs).Sum / _args.Length;! }! private int[] ModifyArgs {! get { return _args.Select(a => a * MAGIC_COEF).ToArray(); }! }! }!! [TestFixture]! public class MyAverageTest {! [Test]! public void Calc() {! var avg = new MyAverage(10, 20);! Assert.AreEqual(30, avg.Calc());! // Но, там же еще всего так много: MAGIC_COEF, ModifyArgs.! }! }!

ВХОДЯЩИЕ ЗАПРОСЫ

Правило 2 !

Тестируем интерфейсы, а не конкретную реализацию

R

ВХОДЯЩИЕ КОМАНДЫ! private class MyAverage2 : MyAverage {! private int _coef;!! public void SetCoef(int value) {! _coef = value;! }! ! public int CurrentMagicCoef { get { return _coef + MAGIC_COEF; } }! }!!

ВХОДЯЩИЕ КОМАНДЫ! private class MyAverage2 : MyAverage {! private int _coef;!! public void SetCoef(int value) {! _coef = value;! }! ! public int CurrentMagicCoef { get { return _coef + MAGIC_COEF; } }! }!! [TestFixture]! public class MyAverage2Test {! [Test]! public void SetCoef() {! var avg = new MyAverage2();! avg.SetCoef(1);! Assert.AreEqual(3, avg.CurrentMagicCoef);! }! }!

ВХОДЯЩИЕ КОМАНДЫ

Правило 3 !

Тестируем входящие команды по ближайшему публичному, побочному эффекту

R

ЧТО И КАК ТЕСТИРОВАТЬ

Запросы Команды

Входящие Проверка результата

Проверка публичного, побочного эффекта

Самому себе

Исходящие

СООБЩЕНИЯ САМОМУ СЕБЕ

ЗАПРОСЫ К СЕБЕ! private class MyAverage4 {! public const int MAGIC_COEF = 2;! private readonly int[] _args;! public MyAverage4(params int[] args) {! _args = args;! }! public int Calc() {! return new Aggregator(ModifyArgs).Sum / _args.Length;! }! internal int[] ModifyArgs {! get { return _args.Select(a => a * MAGIC_COEF).ToArray(); }! }! }!!!

ЗАПРОСЫ К СЕБЕ! private class MyAverage4 {! public const int MAGIC_COEF = 2;! private readonly int[] _args;! public MyAverage4(params int[] args) {! _args = args;! }! public int Calc() {! return new Aggregator(ModifyArgs).Sum / _args.Length;! }! internal int[] ModifyArgs {! get { return _args.Select(a => a * MAGIC_COEF).ToArray(); }! }! }!!!

ЗАПРОСЫ К СЕБЕ! private class MyAverage4 {! public const int MAGIC_COEF = 2;! private readonly int[] _args;! public MyAverage4(params int[] args) {! _args = args;! }! public int Calc() {! return new Aggregator(ModifyArgs).Sum / _args.Length;! }! internal int[] ModifyArgs {! get { return _args.Select(a => a * MAGIC_COEF).ToArray(); }! }! }!! [TestFixture]! public class MyAverage4Test {! [Test]! public void ModifyArgs() {! var avg = new MyAverage4(10);! Assert.AreEqual(20, avg.ModifyArgs);! }! }!

ЗАПРОСЫ К СЕБЕ! private class MyAverage4 {! public const int MAGIC_COEF = 2;! private readonly int[] _args;! public MyAverage4(params int[] args) {! _args = args;! }! public int Calc() {! return new Aggregator(ModifyArgs).Sum / _args.Length;! }! internal int[] ModifyArgs {! get { return _args.Select(a => a * MAGIC_COEF).ToArray(); }! }! }!! [TestFixture]! public class MyAverage4Test {! [Test]! public void ModifyArgs() {! var avg = new MyAverage4(10);! Assert.AreEqual(20, avg.ModifyArgs);! }! }!

ТАК ДЕЛАТЬ НЕ НАДО

КОМАНДЫ К СЕБЕ! private class MyAverage5 {! private readonly int[] _args;! internal Aggregator Aggregator;! public MyAverage5(params int[] args) {! _args = args;! }! public int Calc() {! InitAggregator()! return Aggregator.Sum / _args.Length;! }! private void InitAggregator() {! Aggregator = new Aggregator(_args);! }! }!

КОМАНДЫ К СЕБЕ! private class MyAverage5 {! private readonly int[] _args;! internal Aggregator Aggregator;! public MyAverage5(params int[] args) {! _args = args;! }! public int Calc() {! InitAggregator()! return Aggregator.Sum / _args.Length;! }! private void InitAggregator() {! Aggregator = new Aggregator(_args);! }! }!! [TestFixture]! public class MyAverage5Test {! [Test]! public void Calc() {! var avg = new MyAverage5(10, 20);! Assert.AreEqual(15, avg.Calc());! Assert.IsNotNull(avg.Aggregator);! }! }!

КОМАНДЫ К СЕБЕ! private class MyAverage5 {! private readonly int[] _args;! internal Aggregator Aggregator;! public MyAverage5(params int[] args) {! _args = args;! }! public int Calc() {! InitAggregator()! return Aggregator.Sum / _args.Length;! }! private void InitAggregator() {! Aggregator = new Aggregator(_args);! }! }!! [TestFixture]! public class MyAverage5Test {! [Test]! public void Calc() {! var avg = new MyAverage5(10, 20);! Assert.AreEqual(15, avg.Calc());! Assert.IsNotNull(avg.Aggregator);! }! }!

Этому тут не место

КОМАНДЫ К СЕБЕ! private class MyAverage5 {! private readonly int[] _args;! internal Aggregator Aggregator;! public MyAverage5(params int[] args) {! _args = args;! }! public int Calc() {! InitAggregator()! return Aggregator.Sum / _args.Length;! }! private void InitAggregator() {! Aggregator = new Aggregator(_args);! }! }!! [TestFixture]! public class MyAverage5Test {! [Test]! public void Calc() {! var avg = new MyAverage5(10, 20);! Assert.AreEqual(15, avg.Calc());! Assert.IsNotNull(avg.Aggregator);! }! }!

ТАК ДЕЛАТЬ НЕ НАДО

СООБЩЕНИЯ САМОМУ СЕБЕ

Правило 4 !

Не тестируйте внутренние методы - не проверяйте их результат - не ожидайте их вызова

R

ЧТО И КАК ТЕСТИРОВАТЬ

Запросы Команды

Входящие Проверка результата

Проверка публичного, побочного эффекта

Самому себе Игнорируем Игнорируем

Исходящие

ИСХОДЯЩИЕ СООБЩЕНИЯ

ИСХОДЯЩИЕ ЗАПРОСЫ! public interface IMagicCoef {! int Value { get; }! }! class MagicCoef : IMagicCoef {! public int Value { ! get { return /* Find Magic Value... */ default(int); } }! }!! private class MyAverage6 {! private readonly IMagicCoef _magic;! private readonly int[] _args;! public MyAverage6(IMagicCoef magic = null, params int[] args) {! _magic = magic ?? new MagicCoef();! _args = args;! }! public int Calc() {! return new Aggregator(ModifyArgs).Sum / _args.Length;! }! private int[] ModifyArgs {! get { return _args.Select(a => a * _magic.Value).ToArray(); }! }! }!

ИСХОДЯЩИЕ ЗАПРОСЫ! public interface IMagicCoef {! int Value { get; }! }! class MagicCoef : IMagicCoef {! public int Value { ! get { return /* Find Magic Value... */ default(int); } }! }!! private class MyAverage6 {! private readonly IMagicCoef _magic;! private readonly int[] _args;! public MyAverage6(IMagicCoef magic = null, params int[] args) {! _magic = magic ?? new MagicCoef();! _args = args;! }! public int Calc() {! return new Aggregator(ModifyArgs).Sum / _args.Length;! }! private int[] ModifyArgs {! get { return _args.Select(a => a * _magic.Value).ToArray(); }! }! }!

ИСХОДЯЩИЕ ЗАПРОСЫ! class MagicCoef : IMagicCoef {! public int Value { ! get { return /* Find Magic Value... */ default(int); } }! }!! private class MyAverage6 {! ...! public int Calc() {! return new Aggregator(ModifyArgs).Sum / _args.Length;! }! private int[] ModifyArgs {! get { return _args.Select(a => a * _magic.Value).ToArray(); }! }! }!! [TestFixture]! public class MyAverage6Test {! [Test]! public void CallMagic() {! var mock = new Mock<IMagicCoef>();! new MyAverage6(mock.Object, 1, 2).Calc();! mock.Verify(m => m.Value, Times.Exactly(2));! }! }!!

ИСХОДЯЩИЕ ЗАПРОСЫ! class MagicCoef : IMagicCoef {! public int Value { ! get { return /* Find Magic Value... */ default(int); } }! }!! private class MyAverage6 {! ...! public int Calc() {! return new Aggregator(ModifyArgs).Sum / _args.Length;! }! private int[] ModifyArgs {! get { return _args.Select(a => a * _magic.Value).ToArray(); }! }! }!! [TestFixture]! public class MyAverage6Test {! [Test]! public void CallMagic() {! var mock = new Mock<IMagicCoef>();! new MyAverage6(mock.Object, 1, 2).Calc();! mock.Verify(m => m.Value, Times.Exactly(2));! }! }!!

ТАК ДЕЛАТЬ НЕ НАДО

ИСХОДЯЩИЕ ЗАПРОСЫ

Правило 5 !

Не тестируйте исходящие запросы - не проверяйте их результат - не ожидайте их вызова

R

ИСХОДЯЩИЕ ЗАПРОСЫ

Правило 5 !

Не тестируйте исходящие запросы - не проверяйте их результат - не ожидайте их вызова

R

Проверка исходящих запросов добавляет стоимости, но не приносит пользы

i

ИСХОДЯЩИЕ КОМАНДЫ!! private class Aggregator7 {! private readonly ILogger _logger;! private readonly int[] _args;! public Aggregator7(ILogger logger = null, params int[] args) {! _logger = logger ?? new LoggerByDb();! _args = args;! }!! public int Sum {! get {! var sum = _args.Sum();! _logger.LogSum(sum);! return sum;! }! }! }!! public interface ILogger {! void LogSum(int sum);! }!! private class LoggerByDb : ILogger {! public void LogSum(int sum) {! // Log sum in data base...! }! }!

ИСХОДЯЩИЕ КОМАНДЫ!! private class Aggregator7 {! private readonly ILogger _logger;! private readonly int[] _args;! public Aggregator7(ILogger logger = null, params int[] args) {! _logger = logger ?? new LoggerByDb();! _args = args;! }!! public int Sum {! get {! var sum = _args.Sum();! _logger.LogSum(sum);! return sum;! }! }! }!! public interface ILogger {! void LogSum(int sum);! }!! private class LoggerByDb : ILogger {! public void LogSum(int sum) {! // Log sum in data base...! }! }!

ИСХОДЯЩИЕ КОМАНДЫ! [TestFixture]! public class Aggregator7Test {! [Test]! public void LogTest() {! var sum = new Aggregator7(new LoggerByDb(), 1, 2, 3).Sum;! var logSum = GetLogsFromDb();! Assert.AreEqual(sum, logSum);! }!! private int GetLogsFromDb() {! // select sum from logtable limit 1;! return default (int);! }! }!

ИСХОДЯЩИЕ КОМАНДЫ! [TestFixture]! public class Aggregator7Test {! [Test]! public void LogTest() {! var sum = new Aggregator7(new LoggerByDb(), 1, 2, 3).Sum;! var logSum = GetLogsFromDb();! Assert.AreEqual(sum, logSum);! }!! private int GetLogsFromDb() {! // select sum from logtable limit 1;! return default (int);! }! }!

Это зависимость от «далеких» эффектов и конкретной реализации. Замедляет тесты.

i

ИСХОДЯЩИЕ КОМАНДЫ! [TestFixture]! public class Aggregator7Test {! [Test]! public void LogTest() {! var sum = new Aggregator7(new LoggerByDb(), 1, 2, 3).Sum;! var logSum = GetLogsFromDb();! Assert.AreEqual(sum, logSum);! }!! private int GetLogsFromDb() {! // select sum from logtable limit 1;! return default (int);! }! }!

Это зависимость от «далеких» эффектов и конкретной реализации. Замедляет тесты.

Главное - не имеет отношение к ответственности класса.

i

ИСХОДЯЩИЕ КОМАНДЫ! [TestFixture]! public class Aggregator7Test {! [Test]! public void LogTest() {! var sum = new Aggregator7(new LoggerByDb(), 1, 2, 3).Sum;! var logSum = GetLogsFromDb();! Assert.AreEqual(sum, logSum);! }!! private int GetLogsFromDb() {! // select sum from logtable limit 1;! return default (int);! }! }!

Это зависимость от «далеких» эффектов и конкретной реализации. Замедляет тесты.

Главное - не имеет отношение к ответственности класса.

i

ТАК ДЕЛАТЬ НЕ НАДО

ИСХОДЯЩИЕ КОМАНДЫ! [TestFixture]! public class Aggregator7Test {! // Anti pattern! [Test]! public void BadLogTest() {! var sum = new Aggregator7(new LoggerByDb(), 1, 2, 3).Sum;! var logSum = GetLogsFromDb();! Assert.AreEqual(sum, logSum);! }!! private int GetLogsFromDb() {! // select sum from logtable limit 1;! return default (int);! }!! // Good! [Test]! public void GoodLogTest() {! var m = new Mock<ILogger>();! var res = new Aggregator7(m.Object, 1, 2, 3).Sum;! m.Verify(e => e.LogSum(6), Times.Once());! }! }!

ИСХОДЯЩИЕ КОМАНДЫ! [TestFixture]! public class Aggregator7Test {! // Anti pattern! [Test]! public void BadLogTest() {! var sum = new Aggregator7(new LoggerByDb(), 1, 2, 3).Sum;! var logSum = GetLogsFromDb();! Assert.AreEqual(sum, logSum);! }!! private int GetLogsFromDb() {! // select sum from logtable limit 1;! return default (int);! }!! // Good! [Test]! public void GoodLogTest() {! var m = new Mock<ILogger>();! var res = new Aggregator7(m.Object, 1, 2, 3).Sum;! m.Verify(e => e.LogSum(6), Times.Once());! }! }!

Ответственность класса - отправить командуi

ИСХОДЯЩИЕ КОМАНДЫ

Правило 6 !

Проверяйте отправку исходящих команд с определенными параметрами, но не более того

R

ИСХОДЯЩИЕ КОМАНДЫ

Правило 6 !

Проверяйте отправку исходящих команд с определенными параметрами, но не более того

R

НО...

ИСХОДЯЩИЕ КОМАНДЫ

Что будет если интерфейс ILogger захочется изменить?

ИСХОДЯЩИЕ КОМАНДЫ

Что будет если интерфейс ILogger захочется изменить?

БОЛЬ И СТРАДАНИЕ

ИСХОДЯЩИЕ КОМАНДЫ

Что будет если интерфейс ILogger захочется изменить?

БОЛЬ И СТРАДАНИЕ

Создавайте «тоникие» интерфейсы. Соблюдайте контракты

i

ЧТО И КАК ТЕСТИРОВАТЬ

Запросы Команды

Входящие Проверка результата

Проверка публичного, побочного эффекта

Самому себе Игнорируем Игнорируем

Исходящие Игнорируем Ожидание вызова

ИТОГО

БУДЬТЕ МИНИМАЛИСТОМ

БУДЬТЕ МИНИМАЛИСТОМв хорошем смысле

ТЕСТИРУЙТЕ ВСЕ

ТЕСТИРУЙТЕ ВСЕ

Только ОДИН раз

НАСТАИВАЙТЕ НА ПРОСТОТЕ

У каждого есть возможность сказать:

У каждого есть возможность сказать:

«Я обожаю свои тесты»

ВОПРОСЫ

TDD

Внимание! Очень сложно, но крайне важно

... это всё

ВОПРОСЫ

ССЫЛКИ

• http://en.wikipedia.org/wiki/Test-driven_development

• https://speakerdeck.com/skmetz/magic-tricks-of-testing-railsconf

Практическая часть

ВОПРОСЫ