[Fact] public void AcceptOrder_Successful() { var ingredient1 = new Ingredient("Ingredient1"); var ingredient2 = new Ingredient("Ingredient2"); var ingredient3 = new Ingredient("Ingredient3"); var order = new Order(DateTime.Now); var product1 = new Product("Pizza1"); product1.AddIngredient(ingredient1); product1.AddIngredient(ingredient2); var orderLine1 = new OrderLine(product1, 1, 500); order.AddLine(orderLine1); var product2 = new Product("Pizza2"); product2.AddIngredient(ingredient1); product2.AddIngredient(ingredient3); var orderLine2 = new OrderLine(product2, 1, 650); order.AddLine(orderLine2); var orderRepositoryMock = new Mock<IOrderRepository>(); var ingredientsRepositoryMock = new Mock<IIngredientRepository>(); var service = new PizzeriaService(orderRepositoryMock.Object, ingredientsRepositoryMock.Object); service.AcceptOrder(order); orderRepositoryMock.Verify(r => r.Add(order), Times.Once); ingredientsRepositoryMock.Verify(r => r.ReserveIngredients(order), Times.Once); }
If it is not possible to concisely name the test, then something is wrong with the test.Since the test is incomprehensible, let's tell what is happening in it:
public static class Ingredients { public static readonly Ingredient Dough = new Ingredient("Dough"); public static readonly Ingredient Pepperoni = new Ingredient("Pepperoni"); public static readonly Ingredient Mozzarella = new Ingredient("Mozzarella"); }
Use domain object predefined for frequently used domain entities.
public class ProductBuilder { private Product _product; public ProductBuilder(string name) { _product = new Product(name); } public ProductBuilder Containing(Ingredient ingredient) { _product.AddIngredient(ingredient); return this; } public Product Please() { return _product; } }
Containing
and a terminal method Please
. If you do not like to be kind to the code, then you can replace Please
with Now
. The builder hides complex constructors and method calls that customize the object. The code becomes cleaner and clearer. In a good way, the builder should simplify the creation of the object so that the code is understandable to the domain expert. Especially it is worth using a builder for objects that require configuration before starting work. var pepperoni = new ProductBuilder("Pepperoni") .Containing(Ingredients.Dough) .Containing(Ingredients.Pepperoni) .Please();
Builders help create objects that need customization. Consider creating a builder even if the setting consists of a single line.
new ProductBuilder
still looks pretty ugly. Fix it with the ObjectMother (Father) pattern. public static class Create { public static ProductBuilder Product(string name) => new ProductBuilder(name); }
var pepperoni = Create.Product("Pepperoni") .Containing(Ingredients.Dough) .Containing(Ingredients.Pepperoni) .Please();
Create
IDE itself will prompt what can be created in this domain.Create
, but Given
. I like both options. If you have any other ideas - share in the comments.For declarative creation of objects use ObjectMother. The code will be cleaner, and it will be easier for new developers to delve into the domain.
public static class Pizza { public static Product Pepperoni => Create.Product("Pepperoni") .Containing(Ingredients.Dough) .Containing(Ingredients.Pepperoni) .Please(); public static Product Margarita => Create.Product("Margarita") .Containing(Ingredients.Dough) .Containing(Ingredients.Mozzarella) .Please(); }
Products
, but Pizza
. This name helps to read the test. For example, it helps to remove questions like “Is Pepperoni a pizza or sausage?”.Try to use real domain objects, not substitutes like Product1.
var order = Create.Order .Dated(DateTime.Now) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please();
public class OrderBuilder { private DateTime _date; private readonly List<OrderLine> _lines = new List<OrderLine>(); public OrderBuilder Dated(DateTime date) { _date = date; return this; } public OrderBuilder With(OrderLine orderLine) { _lines.Add(orderLine); return this; } public Order Please() { var order = new Order(_date); foreach (var line in _lines) { order.AddLine(line); } return order; } }
OrderLine
situation is more interesting: firstly, the terminal method Please is not called here, and secondly, the access to the builder is not provided by the static Create
or the constructor of the builder itself. We will solve the first problem with the help of an implicit operator
and our builder will look like this: public class OrderLineBuilder { private Product _product; private decimal _count; private decimal _price; public OrderLineBuilder Of(decimal count, Product product) { _product = product; _count = count; return this; } public OrderLineBuilder For(decimal price) { _price = price; return this; } public static implicit operator OrderLine(OrderLineBuilder b) { return new OrderLine(b._product, b._count, b._price); } }
Product
class will help us to understand: public static class ProductExtensions { public static OrderLineBuilder CountOf(this Product product, decimal count) { return Create.OrderLine.Of(count, product) } }
Use extension methods. Just use them. :)Having done all these actions, we received the following test code:
[Fact] public void AcceptOrder_Successful() { var order = Create.Order .Dated(DateTime.Now) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var orderRepositoryMock = new Mock<IOrderRepository>(); var ingredientsRepositoryMock = new Mock<IIngredientRepository>(); var service = new PizzeriaService(orderRepositoryMock.Object, ingredientsRepositoryMock.Object); service.AcceptOrder(order); orderRepositoryMock.Verify(r => r.Add(order), Times.Once); ingredientsRepositoryMock.Verify(r => r.ReserveIngredients(order), Times.Once); }
Imagine that a magic fairy flew in and allowed you to write the code as you like, and then try to wrap the writing in DSL.
PizzeriaService
, in which I encapsulate all the logic that checks the method calls: public class PizzeriaServiceTestable : PizzeriaService { private readonly Mock<IOrderRepository> _orderRepositoryMock; private readonly Mock<IIngredientRepository> _ingredientRepositoryMock; public PizzeriaServiceTestable(Mock<IOrderRepository> orderRepositoryMock, Mock<IIngredientRepository> ingredientRepositoryMock) : base(orderRepositoryMock.Object, ingredientRepositoryMock.Object) { _orderRepositoryMock = orderRepositoryMock; _ingredientRepositoryMock = ingredientRepositoryMock; } public void VerifyAddWasCalledWith(Order order) { _orderRepositoryMock.Verify(r => r.Add(order), Times.Once); } public void VerifyReserveIngredientsWasCalledWith(Order order) { _ingredientRepositoryMock.Verify(r => r.ReserveIngredients(order), Times.Once); } }
public class PizzeriaServiceBuilder { public PizzeriaServiceTestable Please() { var orderRepositoryMock = new Mock<IOrderRepository>(); var ingredientsRepositoryMock = new Mock<IIngredientRepository>(); return new PizzeriaServiceTestable(orderRepositoryMock, ingredientsRepositoryMock); } }
[Fact] public void AcceptOrder_Successful() { var order = Create.Order .Dated(DateTime.Now) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var service = Create.PizzeriaService.Please(); service.AcceptOrder(order); service.VerifyAddWasCalledWith(order); service.VerifyReserveIngredientsWasCalledWith(order); }
Testable is able to save the situation in the most difficult cases. For behavioral tests, it helps to wrap ugly call checks into beautiful methods.At this momentous moment we finished to understand the clarity of the test. It remains to make it reproducible and focused.
DateTime.Now
as the date of the order. If suddenly, starting from a certain date, the logic of receiving an order changes, then in our business logic we will have at least some time to maintain 2 order acceptance logic, sharing them with a check like if (order.Date > edgeDate)
. In this case, our test has a chance to fall when passing the system date across the boundary. Yes, we will fix it quickly, and even make two tests out of one: one will check the logic before the boundary date, and the other after. Nevertheless, it is better to avoid such situations and immediately make all input data constant.3.May(2019)
. This form of recording will be clear not only to developers, but also to business. To do this, just create such a static class. public static class DateConstructionExtensions { public static DateTime May(this int day, int year) => new DateTime(year, 5, day); }
42.Grams("flour")
.It is convenient to create quantitative objects and dates through the already familiar extension-methods.
[Fact] public void WhenAcceptOrder_AddIsCalled() { var order = Create.Order .Dated(3.May(2019)) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var service = Create.PizzeriaService.Please(); service.AcceptOrder(order); service.VerifyAddWasCalledWith(order); } [Fact] public void WhenAcceptOrder_ReserveIngredientsIsCalled() { var order = Create.Order .Dated(3.May(2019)) .With(Pizza.Pepperoni.CountOf(1).For(500)) .With(Pizza.Margarita.CountOf(1).For(650)) .Please(); var service = Create.PizzeriaService.Please(); service.AcceptOrder(order); service.VerifyReserveIngredientsWasCalledWith(order); }
The focus of the tests makes them supported. A good test must be focused.And now, I already hear you shouting to me, “Yura, have you been fucking? We wrote a million code just to make a couple of tests pretty? ”. Yes exactly. While we have only a couple of tests, it makes sense to invest in DSL and make these tests understandable. Once you write a DSL, you get a bunch of buns:
Source: https://habr.com/ru/post/451598/
All Articles