Отказ от ответственности: это не публикация «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 и поместить весь тест в блок использования .
И поэтому, создавая состояние в каждом тесте, мы следим за тем, чтобы они не могли влиять друг на друга.
Удачного кодирования …