Отказ от ответственности: это не публикация «TDD против BDD» — теперь, когда мы обдумали это, давайте обсудим то, что я больше всего ненавижу в BDD…
Я недавно начал использовать BDD (снова). Мои тесты все еще являются «модульными тестами» — они не вызывают ни базу данных, ни какие-либо другие внешние зависимости, и, поскольку даже при написании модульных тестов я стремлюсь протестировать функциональность (мини-требования), большинство из них не изменились слишком сильно.
В качестве предмета этого эксперимента я выбрал BDDfy, который идеально подходил для моих нужд.
В некоторых средах BDD используются два «вида» файлов — текстовые файлы, в которых записаны сценарии и истории (в виде простого текста), и файлы кода, которые реализуют функции, соответствующие шагам в этих сценариях.
BDDfy, с другой стороны, использует файл кода, который также используется для генерации спецификации, поэтому для меня меньше работы.
Используя BDDfy, я могу преобразовать следующий модульный тест:
[TestMethod]
public void RejectCall_UserRecieveCallAndRejectIt_SendReport()
{
var fakeMessageEngine = A.Fake<IMessageEngine>();
var phone = new Phone(fakeMessageEngine);
var notificationService = new NotificationService(phone);
var message = new NewCallMessage("otherUserId");
fakeMessageEngine.OnMessageArrived += Raise.With(new MessageArrivedEventArgs(message)).Now;
phone.AnswerCall(Answer.Reject);
var result = notificationService.GetLastReport();
Assert.AreEqual(ReportType.CallEnded, result.ReportType);
}
Чтобы что-то вроде этого:
[TestClass]
public class NotificationBddTests
{
IMessageEngine _fakeMessageEngine;
Phone _phone;
NotificationService _notificationService;
public NotificationBddTests()
{
_fakeMessageEngine = A.Fake<IMessageEngine>();
_phone = new Phone(_fakeMessageEngine);
_notificationService = new NotificationService(_phone);
}
[TestMethod]
public void RejectCall_UserRecieveCallAndRejectIt_SendReport()
{
new NotificationBddTests()
.Given(s => s.RecievedCallFromUser())
.When(s => s.CallRejected())
.Then(s => s.EndCallReportAdded())
.BDDfy();
}
private void RecievedCallFromUser()
{
var message = new NewCallMessage("otherUserId");
_fakeMessageEngine.OnMessageArrived += Raise.With(new MessageArrivedEventArgs(message)).Now;
}
private void CallRejected()
{
_phone.AnswerCall(Answer.Reject);
}
private void EndCallReportAdded()
{
var result = _notificationService.GetLastReport();
Assert.AreEqual(ReportType.CallEnded, result.ReportType);
}
}
И это улучшение — тест более читабелен и хорошо организован.
Но есть скрытая «цена» за написание такого рода тестов — которые могут вызвать хрупкие тесты и проблемы со стабильностью в ближайшем будущем — можете ли вы определить проблему?
Все дело в общем состоянии
Я должен сделать признание: большинство примеров кода BDD в Интернете заставляют меня съеживаться. Им всем присущ один и тот же недостаток. Все дело в том, «как мы тестируем реальные требования» и «создаем живую спецификацию», но никто никогда не говорит о том факте, что приведенный выше тест может стать огромной проблемой в обслуживании!
Проблема может быть легко показана на следующем примере:
[TestClass]
public class ShoppingCartTests
{
private readonly ShoppingCart _cart = new ShoppingCart()
[TestMethod]
public void EmptyCartTest()
{
new ShoppingCartTests()
.Given(s => s.ShoppingCartIsEmpty())
.Then(s => s.TotalPriceEquals(0))
.BDDfy();
}
[TestMethod]
public void AddItemTest()
{
var newProduct = new Product(id: "prd-1", price: 100
new ShoppingCartTests()
.Given(s => s.ShoppingCartIsEmpty())
.When(s => s.AddProductToCart(newProduct))
.Then(s => s.TotalPriceEquals(newProduct.Price))
.BDDfy();
}
private void ShoppingCartIsEmpty() { }
private void TotalPriceEquals(int expected)
{
Assert.AreEqual(expected, _cart.TotalPrice);
}
private void AddProductToCart(Product product)
{
_cart.Add(product);
}
}
Видите ли вы поле, которое используется всеми тестами.
Используя один и тот же экземпляр во всех тестах, мы случайно запускаем (или не выполняем) тест в зависимости от порядка их запуска. Очевидно, это легко исправить (в данном случае). К сожалению, такая простая проблема может стать огромной проблемой, чем больше написано тестов, тем больше требуется логики инициализации, и, прежде чем вы это узнаете, наступает середина ночи, и вы пытаетесь понять выяснить, как мог тест, который раньше работал, терпеть неудачу иногда — без видимой причины.
Это не проблема BDD — я могу легко написать (и написать) один и тот же код без помощи среды BDD. Однако кажется, что вся структура BDD подталкивает вас к этой проблеме.
Решение
К счастью, эту проблему легко избежать полностью — убедитесь, что тесты не разделяют одно и то же состояние (я сказал, что это легко).
Я создал новый класс с именем NotificationTestBuilder (потому что наименование вещей — самая сложная вещь в программировании) и поместил в него все «состояние» теста.
И теперь я могу написать один и тот же тест BDD, используя одно состояние на тест:
public class NotificationTestBuilder
{
IMessageEngine _fakeMessageEngine;
Phone _phone;
NotificationService _notificationService;
public NotificationTestBuilder()
{
_fakeMessageEngine = A.Fake<IMessageEngine>();
_phone = new Phone(_fakeMessageEngine);
_notificationService = new NotificationService(_phone);
}
public void RecievedCallFromUser()
{
var message = new NewCallMessage("otherUserId");
_fakeMessageEngine.OnMessageArrived += Raise.With(new MessageArrivedEventArgs(message)).Now;
}
public void CallRejected()
{
_phone.AnswerCall(Answer.Reject);
}
public void EndCallReportAdded()
{
var result = _notificationService.GetLastReport();
Assert.AreEqual(ReportType.CallEnded, result.ReportType);
}
}
[TestClass]
public class NotificationBddTests
{
[TestMethod]
public void RejectCall_UserRecieveCallAndRejectIt_SendReport()
{
var builder = new NotificationTestBuilder();
new NotificationBddTests()
.Given(s => builder.RecievedCallFromUser())
.When(s => builder.CallRejected())
.Then(s => builder.EndCallReportAdded())
.BDDfy();
}
}
И если мне нужно было провести «очистку» после завершения теста — я могу реализовать IDisposable и поместить весь тест в блок использования .
И поэтому, создавая состояние в каждом тесте, мы следим за тем, чтобы они не могли влиять друг на друга.
Удачного кодирования …