#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
