понедельник, 16 марта 2009 г.

Необычный подход к написанию юнит тестов

Обнаружил довольно необычный подход к написанию юнит тестов у автора вот этого поста:
Test Driven Design и Test First Development -- в чем разница?

Меня привлекло то, что тест класс представляет из себя наследник тестируемого класса.

Приведу пример классического и альтернативного подхода (пример из документации NUnit).

Тестируемый класс:
namespace bank
{
public class Account
{
private float balance;
public void Deposit(float amount)
{
balance+=amount;
}

public void Withdraw(float amount)
{
balance-=amount;
}

public void TransferFunds(Account destination, float amount)
{
}

public float Balance
{
get{ return balance;}
}
}
}
Тест, написаный при помощи "классического" подхода:
namespace bank
{
using NUnit.Framework;

[TestFixture]
public class AccountTest
{
[Test]
public void TransferFunds()
{
Account source = new Account();
source.Deposit(200.00F);
Account destination = new Account();
destination.Deposit(150.00F);

source.TransferFunds(destination, 100.00F);
Assert.AreEqual(250.00F, destination.Balance);
Assert.AreEqual(100.00F, source.Balance);

}
}
}
Тест, написанный с помощью альтернативного подхода с наследованием:
namespace bank
{
using NUnit.Framework;

[TestFixture]
public class AccountTest : Account
{
[Test]
public void TestTransferFunds()
{
this.Deposit(200.00F);
Account destination = new Account();
destination.Deposit(150.00F);

this.TransferFunds(destination, 100.00F);
Assert.AreEqual(250.00F, destination.Balance);
Assert.AreEqual(100.00F, this.Balance);
}
}
}

Сразу отмечу, что конкретно в этом примере второй вариант, вероятно, выглядит неудачно. Скорее всего в каких-то случаях наследование, возможно, выглядит более привлекательно.


Лично у меня возникает вопрос. А стоит ли вообще так писать тесты?

У меня сходу возникли следующие мысли насчет альтернативного подхода:
+ Наследование позволяет протестировать protected методы
+ Я могу написать helper-методы в тест классе, который будет активно работать с protected методами и свойствами
- Наследование дает бОльшую связность между тестовым и тестируемым классом
- Наследование скорее всего завяжет мне руки в SetUp/TearDown методах
- Если класс sealed, то о подходе с наследованием можно забыть
- Когда я пишу классический тест-метод, то я получаю лаконичную документацию к своему коду в виде маленьких примеров
- Когда я пишу классический тест-метод, то я использую класс точно так же как это делает production-код
- У класса Account будет аж два клиента - production code and unit tests, что как правило будет положительно отражаться на его интерфейсе


Очень интересны ваши мнения на этот счет.

3 комментария:

Kostiantyn Kolesnichenko комментирует...

ИМХО, то, что ты отметил как минусы "альтернативного" подхода - это и правда минусы, а плюсы - призрачны.

ИМХО, тестирования достоин только публичный интерфейс, предоставляемый юнитом. Если следовать принципам СОЛИД, то тестировать защищённые методы класса - это нарушение всего и вся. И ведь неясно зачем - ведь цель тестирования - проявить недостатки класса при его реальном использовании (т.е. через публичный интерфейс), не так ли?

Alexey Diyan комментирует...

Полностью согласен с тобой. Но все-таки программист должен быть человеком прагматичным ;)

Вот поэтому я пытался привести хотя бы какие-нибудь аргументы в пользу подхода с наследованием.

Ведь если этот подход существует, значит он кому-то удобен.

Аргументы в пользу наследования действительно не очень убедительные. Вероятно, это как раз из-за того, что я тоже не разделяю этот подход к написанию тестов.

Kostiantyn Kolesnichenko комментирует...

Только что в голову пришла мысль, когда тестирование через наследование - необходимо. А именно: когда тестированию надо подвергнуть какой-то контрол, от которого позволено наследоваться "пользователям" (допустим я есть изготовитель ГУИ набора контролов). Тогда действительно, мне НЕОБХОДИМО каким-то образом получить доступ к защищённым методам, потому что в данной ситуации они выступают тем самым интерфейсом реального использования.