#3 Recepta na – Unit Test + Object Builder
Dzisiejsza recepta będzie dotyczyła obiektów i sposobu ich konstruowania na potrzeby testów jednostkowych. Zastosowany wzorzec nie ogranicza się tylko do projektów testowych i jak najbardziej można go używać w innych warstwach.
Builder Design Pattern
Jak głosi wikipedia i inne źródła wzorzec budowniczy (prawda, że fajne tłumaczenie przyjęto 😃) wspomaga konstruowanie obiektów. Pozwala odseparować reprezentację obiektu (pola, właściwości) od pocesu jego konstrukcji. Tyle z teorii.
Kiedy używać
Kodzimy
public class Order { public OrderInfo OrderInfo { get; private set; } public ShipmentMethod Shipment { get; private set; } public ICollection<OrderItem> Items { get; private set; } } public class OrderInfo { public Customer Customer { get; private set; } } public class Customer { public Address Addres { get; private set; } } public class Address { } public class OrderItem { } public class ShipmentMethod { }
[Test] public void My_First_Test() { //Prepare some order var order = new Order(); order.OrderInfo = new OrderInfo { Customer = new Customer { //fill customer properties ... } }; order.Shipment = new ShipmentMethod { //fill shipment info ... }; order.Items = new List<OrderItem> { //add some items to order ... new OrderItem { }, new OrderItem{ } }; //Act //Assert }
Wyjście zastępcze
[Test] public void My_First_Test() { var order = GetSampleOrder(); //Act //Assert } [Test] public void My_Second_Test() { var order = GetSampleOrder(); //Act //Assert } private Order GetSampleOrder() { //Prepare some order var order = new Order(); order.OrderInfo = new OrderInfo { Customer = new Customer { //fill customer properties ... } }; order.Shipment = new ShipmentMethod { //fill shipment info ... }; order.Items = new List<OrderItem> { //add some items to order ... new OrderItem { }, new OrderItem{ } }; return order; }
OrderObjectBuilder
W którym udostępnimy możliwość konstruowania różnych reprezentacji zamówienia. Dodatkowo ta implementacja będzie wykorzystywała tzw. „method chaining”. Pozwolę sobie na bardzo opisowe nazwy metod tak aby wprost pokazać wam ideę.
Krok #1
Utworzenie klasy OrderObjectBuilder z dwiem metodami:
- CreateOrder() – tworzy instancje zamówienia – najprostsza możliwa reprezentacja. Zwróć uwagę na „return this”. To wspomniany „method chaining”. Zwracamy instancję klasy OrderObjectBuilder dzięki czemu będziemy mieli możliwość wywoływania kolejnych jej metod po „kropce” – przykład najlepiej to zaprezentuje.
- Build() – metoda, która zwraca instancję zamówienia. Tą metodę wołamy na samym końcu.
public class OrderObjectBuilder { private Order _order; public OrderObjectBuilder CreateOrder() { _order = new Order(); return this; } public Order Build() { return _order; } }
Przykład użycia:
OrderObjectBuilder _orderBuilder = new OrderObjectBuilder(); [Test] public void My_First_Test() { var myOrder = _orderBuilder .CreateOrder() .Build(); }
„Method chaining” umożliwił zawołanie metody Build() bezpośrednio po CreateOrder().
Krok #2
public OrderObjectBuilder WithCustomer() { //create here sample customer return this; } public OrderObjectBuilder WithCustomer(Customer customer) { _order.OrderInfo.Customer = customer; return this; } public OrderObjectBuilder WithShipment() { //create sample shipment method return this; } public OrderObjectBuilder WithDPDShipment() { //set DPD shipment return this; } public OrderObjectBuilder WithDHLShipment() { //set DHL shipment return this; } public OrderObjectBuilder WithItem() { //add sample item to order _order.Items.Add(new OrderItem()); return this; } public OrderObjectBuilder WithItem(OrderItem orderItem) { _order.Items.Add(orderItem); return this; } public Order Build() { return _order; }
var myOrder = _orderBuilder .CreateOrder() .WithCustomer() .WithDHLShipment() .WithItem() //some random item .WithItem(new OrderItem()) //particular order item .Build(); var orderWithNoItems = _orderBuilder .CreateOrder() .WithCustomer() .WithDPDShipment() .Build(); var orderWithNoShipment = _orderBuilder .CreateOrder() .WithCustomer() .WithItem() .Build();
Podsumowanie
- Uzyskaliśmy separację reprezentacji obiektu od jego konstruowania
- Pozbyliśmy się zbędnego, niewiele wnoszącego do przypadku testowego kodu
- Enkapsulacja zabezpieczy nas przed przyszłymi zmianami
- Method chaining wprowadził czytelne API