Bugün, test odaklı geliştirmedeki builder kalıbından bahsedeceğim. Zaten testlerle çalışıyorsanız, muhtemelen tüm girdi verilerini oluşturmanın ne kadar zaman alıcı olabileceğini fark etmişsinizdir. Genellikle, aynı veri seti veya ufak farklılıkları olan veriler, bir sistemin test takımındaki birçok testte kullanılır. Builder burada yardımcı olur. İki amaca hizmet eder:
Örnek olması açısından Invoice
sınıfını ele alacağım, çok basitleştirilmiş bir versiyonu şöyle olabilir:
public class Invoice { public Invoice( string invoiceNo, string customer, string countryCode, DateTime invoiceDate, IReadOnlyList<InvoiceLine> lines) { InvoiceNo = invoiceNo; InvoiceDate = invoiceDate; Customer = customer; CountryCode = countryCode; Lines = lines; } public string InvoiceNo { get; } public string Customer { get; } public string CountryCode { get; } public DateTime InvoiceDate { get; } public decimal TotalAmount => Lines.Sum(x => x.TotalPrice); public IReadOnlyList<InvoiceLine> Lines { get; } } public class InvoiceLine { public InvoiceLine( string itemCode, decimal unitCount, decimal unitPrice, decimal vat) { ItemCode = itemCode; UnitCount = unitCount; UnitPrice = unitPrice; Vat= vat; } public string ItemCode { get; } public decimal UnitCount { get; } public decimal UnitPrice { get; } public decimal Vat { get; } public decimal TotalPrice => UnitCount * UnitPrice * (1 + Vat / 100); }
Bir Invoice
nesnesi oluşturmak için, Invoice
ve InvoiceLine
oluşturucularına birçok değer sağlamam gerekir. Çoğu durumda, özelliklerin yalnızca bir kısmı belirli testlerle ilgilidir. Burada, oluşturucular yardıma gelir.
InvoiceLine
için Oluşturucu aşağıdaki gibi görünebilir:
public partial class InvoiceLineBuilder { private string _itemCode; private decimal _unitCount; private decimal _unitPrice; private decimal _vat; public static implicit operator InvoiceLine(InvoiceLineBuilder builder) => builder.Build(); public static InvoiceLineBuilder Default() { return new InvoiceLineBuilder( "001", 1, 100, 21 ); } public InvoiceLineBuilder( string itemCode, decimal unitCount, decimal unitPrice, decimal vat) { _itemCode = itemCode; _unitCount = unitCount; _unitPrice = unitPrice; _vat = vat; } public InvoiceLine Build() { return new InvoiceLine( _itemCode, _unitCount, _unitPrice, _vat ); } public InvoiceLineBuilder WithItemCode(string value) { _itemCode = value; return this; } public InvoiceLineBuilder WithUnitCount(decimal value) { _unitCount = value; return this; } public InvoiceLineBuilder WithUnitPrice(decimal value) { _unitPrice = value; return this; } public InvoiceLineBuilder WithVat(decimal vat) { _vat = value; return this; } }
Invoice
Oluşturucu aşağıdaki gibi görünebilir:
public partial class InvoiceBuilder { private string _invoiceNo; private string _customer; private string _countryCode; private DateTime _invoiceDate; private IReadOnlyList<InvoiceLine> _lines; public static implicit operator Invoice(InvoiceBuilder builder) => builder.Build(); public static InvoiceBuilder Default() { return new InvoiceBuilder( "S001", "AB VeryImportantCustomer", "SV", DateTime.Parse("2024-01-01"), new [] { InvoiceLineBuilder .Default() .Build() } ); } public InvoiceBuilder( string invoiceNo, string customer, string countryCode, DateTime invoiceDate, IReadOnlyList<InvoiceLine> lines) { _invoiceNo = invoiceNo; _customer = customer; _countryCode = countryCode; _invoiceDate = invoiceDate; _lines = lines; } public Invoice Build() { return new Invoice( _invoiceNo, _invoiceDate, _lines ); } public InvoiceBuilder WithInvoiceNo(string value) { _invoiceNo = value; return this; } public InvoiceBuilder WithCustomer(string value) { _customer = value; return this; } public InvoiceBuilder WithCountryCode(string value) { _countryCode = value; return this; } public InvoiceBuilder WithInvoiceDate(DateTime value) { _invoiceDate = value; return this; } public InvoiceBuilder WithLines(IReadOnlyList<InvoiceLine> value) { _lines = value; return this; } public InvoiceBuilder WithLines(params InvoiceLine[] value) { _lines = value; return this; } }
Bir testin sadece toplam fiyat özelliği için bir Invoice
nesnesine ihtiyacı olması durumunda, Invoice
şu şekilde oluşturulabilir:
var invoice = InvoiceBuilder .Default() .WithLines( InvoiceLineBuilder .Default .WithUnitPrice(158) );
Toplam fiyat fatura satırlarının toplanmasıyla hesaplandığından ve fatura satırı için varsayılan birim sayısı 1 olduğundan, fatura satırı için birim fiyatı ayarlamak yeterlidir. Birden fazla testte benzer işlevselliğe ihtiyaç duyulursa, daha ileri gidip InvoiceBuilder
aşağıdaki yöntemi ekleyebiliriz:
public static InvoiceBuilder DefaultWithTotalPrice(decimal totalPrice) { return new InvoiceBuilder( "S001", DateTime.Parse("2023-01-01"), new[] { InvoiceLineBuilder .Default() .WithUnitPrice(totalPrice) .Build() } ); }
Yukarıda belirtildiği gibi, builder sınıfı, sınıf için tüm yaygın ve uç durumları toplamak için harika bir yerdir. Burada, bu olası durumlardan birkaçını sunacağım:
Bana göre, sistemimizin ele aldığı farklı durumlar hakkında bilgi toplamak için harika bir yer. Yeni geliştiricilerin sistemin yönetmesi gerekenleri anlamaları için yararlı bir bilgi tabanı görevi görüyor. Bir alanda yeniysem, olası uç durumları düşünmeyebilirim bile. Yukarıda belirtilen durumlardan bazılarından bir kod örneği aşağıdadır:
public static InvoiceBuilder ForEUCountry() { return Default() .WithCountryCode("SV"); } public static InvoiceBuilder ForUSA() { return Default() .WithCountryCode("USA"); } public static InvoiceBuilder ForChina() { return Default() .WithCountryCode("CN"); } public InvoiceBuilder WithRegularVat() { return this .WithLines( InvoiceLineBuilder .Default .WithItemCode("S001") .WithVat(21), InvoiceLineBuilder .Default .WithItemCode("S002") .WithVat(21) ); } public InvoiceBuilder WithReducedVat() { return this .WithLines( InvoiceLineBuilder .Default .WithItemCode("S001") .WithVat(9), InvoiceLineBuilder .Default .WithItemCode("S002") .WithVat(9) ); } public InvoiceBuilder WithMixedVat() { return this .WithLines( InvoiceLineBuilder .Default .WithItemCode("S001") .WithVat(21), InvoiceLineBuilder .Default .WithItemCode("S002") .WithVat(9) ); }
Şimdi yukarıdakilerin bir karışımını oluşturabiliriz. Örneğin, bir test vakası, karışık KDV'ye sahip fatura satırları olan bir AB müşterisi için bir faturaya ihtiyaç duyuyorsa, aşağıdakileri yapabilirim:
[Test] public void SomeTest() { //arrange var invoice = InvoiceBuilder .ForEU() .WithMixedVat(); //act ... //assert ... }
Bu sadece basit bir örnek, ancak kavramı anladığınızı umuyorum.
Büyük ve karmaşık bir nesnemiz olduğunda, ancak testle ilgili yalnızca birkaç alan olduğunda bir oluşturucu yararlıdır.
Başka bir yararlı durum, belirli değerlere dayalı birden fazla senaryoyu test etmek istediğim zamandır. Bir tanesi hariç tüm özellikler aynı kalır ve yalnızca birini değiştiririm. Bu, hizmetin veya nesnenin farklı davranmasına neden olan farkı vurgulamayı kolaylaştırır.
Öncelikle, kendi başınıza bir builder sınıfı oluşturabilirsiniz. Bu, başlangıçta herhangi bir zaman veya para yatırımı gerektirmez ve onu nasıl oluşturacağınız konusunda çok fazla özgürlüğünüz olur. Kopyalama, yapıştırma ve değiştirme faydalı olabilir, ancak yine de oldukça fazla zaman alır, özellikle de daha büyük sınıflar için.
Kod üretimine başladığımda, bunun için tek bir test ayarlayarak başladım. Bu test aslında hiçbir şeyi test etmiyordu; sadece bir türü kabul etti, reflection kullanarak tüm özellikleri aldı, sabit kodlanmış bir şablondan bir builder sınıfı oluşturdu ve bunu test çalıştırıcısının çıktı penceresine yazdı. Tek yapmam gereken bir sınıf dosyası oluşturmak ve içeriği test çalıştırıcısının çıktı penceresinden kopyalayıp yapıştırmaktı.
BuilderGenerator hakkında her şey burada bulunabilir. .NET Incremental Source Generator'ı açıklar. Bu, hedef sınıf değiştiğinde oluşturucu kodunun canlı olarak yeniden oluşturulduğu anlamına gelir. Yani, yukarıdaki yöntemlere kıyasla hiçbir güçlük veya manuel çalışma yoktur. Sadece bir oluşturucu sınıfı oluşturun, hedef sınıf türüyle BuilderFor
niteliğini ekleyin ve tüm With
yöntemleri otomatik olarak oluşturulur ve kullanıma hazır hale gelir.
[BuilderFor(typeof(InvoiceLine))] public partial class InvoiceLineBuilder { public static InvoiceLineBuilder Default() { return new InvoiceLineBuilder() .WithItemCode("S001") .WithUnitCount(1); } }
Çok fazla çalışmadım ama yazının yazıldığı sırada 82.7K indirmeyle geniş bir kullanıcı tabanına sahip gibi görünüyor. Diğer seçenekleri seçmeme neden olan birkaç sorun fark ettim:
Çözüm, builder sınıfı hedef sınıftan farklı bir projedeyse derlemeyi başaramaz. Başka bir projede olabilir, ancak ad alanı aynı kalmalıdır. Aksi takdirde, aşağıdaki hataları görürsünüz::
Oluşturucu parametrelerini desteklemez ve hedef sınıfın parametresiz bir oluşturucusu yoksa hatalarla başarısız olur.
Başka hangi seçeneklerimiz olduğunu inceleyelim.
Bu, yazıldığı tarihte 82,2 milyondan fazla toplam indirme (ve mevcut sürüm için 186,1K) ile oldukça popüler bir kütüphanedir. Kütüphanenin yazarının belirttiği gibi, önceden tanımlanmış kurallara dayalı olarak çok sayıda nesne üretebilen sahte bir veri üretecidir. TDD'deki oluşturucu deseninin tam olarak aynısı değildir, ancak uyarlanabilir.
Bogus.Faker'ı kullanmanın birkaç yolu var, ancak ben burada builder şablonunun nasıl taklit edileceğine odaklanacağım.
Bir nesne yaratmanın en basit yolu Bogus.Faker'dır:
[Test] public void BogusTest() { var faker = new Faker<InvoiceLine2>(); var invoiceLine = faker.Generate(); Assert.IsNotNull(invoiceLine); }
Varsayılan değerlerle, yani null ve sıfırlarla InvoiceLine2
bir örneğini oluşturur. Bazı değerleri ayarlamak için aşağıdaki kurulumu kullanacağım:
[Test] public void BogusTest() { var faker = new Faker<InvoiceLine2>() .RuleFor(x => x.ItemCode, f => f.Random.AlphaNumeric(5)) .RuleFor(x => x.UnitPrice, f => f.Random.Decimal(10, 1000)) .RuleFor(x => x.UnitCount, f => f.Random.Number(1, 5)) .RuleFor(x => x.Vat, f => f.PickRandom(21, 9, 0)); var invoiceLine = faker.Generate(); Assert.IsNotNull(invoiceLine); ToJson(invoiceLine); }
Yukarıdaki kod, rastgele değerlere sahip bir fatura satırı nesnesi oluşturur. Bir örnek şu şekilde görünebilir:
{ "ItemCode": "gwg7y", "UnitCount": 3.0, "UnitPrice": 597.035612417891230, "Vat": 0.0, "TotalPrice": 1791.106837253673690 }
Faydalıdır, ancak her test kendi kurulumunu gerektirir. Bunun yerine bir builder sınıfı oluşturabiliriz:
public class InvoiceLineBuilder: Faker<InvoiceLine2> { public static InvoiceLineBuilder Default() { var faker = new InvoiceLineBuilder(); faker .RuleFor(x => x.ItemCode, f => f.Random.AlphaNumeric(5)) .RuleFor(x => x.UnitPrice, f => f.Random.Decimal(10, 1000)) .RuleFor(x => x.UnitCount, f => f.Random.Number(1, 5)) .RuleFor(x => x.Vat, f => f.PickRandom(21, 9, 0)); return faker; } }
Kullanımı şu şekilde olacaktır:
[Test] public void BogusTest() { var faker = TestDoubles.Bogus.InvoiceLineBuilder .Default() .RuleFor(x => x.ItemCode, f => "S001") .RuleFor(x => x.UnitPrice, f => 100); var invoiceLine = faker.Generate(); Assert.IsNotNull(invoiceLine); ToJson(invoiceLine); }
Ve çıktı:
{ "ItemCode": "S001", "UnitCount": 2.0, "UnitPrice": 100.0, "Vat": 9.0, "TotalPrice": 218.00 }
Benim bakış açıma göre, normal Builder Pattern'dan biraz daha ayrıntılı. Ayrıca, rastgele değerler kullanmaktan hoşlanmıyorum. Büyük bir sorun değil, ancak bir sınıfın özellikleri bir oluşturucu kullanılarak başlatıldığında ve ayarlayıcıları olmadığında sorunlar ortaya çıkıyor. O zaman bir oluşturucu olarak çalışmıyor ve her kurulum statik hale geliyor.
var faker = new InvoiceLineBuilder(); faker .CustomInstantiator(f => new InvoiceLine( f.Random.AlphaNumeric(5), f.Random.Decimal(10, 1000), f.Random.Number(1, 5), f.PickRandom(21, 9, 0) ) );
Bu da 13,2 milyondan fazla toplam indirme (ve mevcut sürüm için 7,2 milyon) ile oldukça popüler bir kütüphanedir. Son zamanlarda aktif olarak geliştirilmemiş olsa da, son sürümü 2019'da yayınlandı. Esasen, Bogus.Faker'a çok benzer. Belirli bir IPropertyNamer uygulayarak rastgele değerler sağlamak için Bogus'u yeniden kullanmak bile mümkün olmalıdır.
Herhangi bir özellik ayarlamadan kullanmayı deneyelim:
[Test] public void NBuilderTest() { var invoiceLine = Builder<InvoiceLine2> .CreateNew() .Build(); Assert.IsNotNull(invoiceLine); ToJson(invoiceLine); }
Aşağıdaki çıktıyı üretir:
{ "ItemCode": "ItemCode1", "UnitCount": 1.0, "UnitPrice": 1.0, "Vat": 1.0, "TotalPrice": 1.01 }
Bu gönderinin amacı yeniden kullanılabilir bir builder sınıfının nasıl oluşturulacağını göstermektir. Başlayalım:
public class InvoiceLineBuilder { public static ISingleObjectBuilder<InvoiceLine2> Default() { return Builder<InvoiceLine2> .CreateNew() .With(x => x.ItemCode, "S001") .With(x => x.UnitCount, 1) .With(x => x.UnitPrice, 100) .With(x => x.Vat, 21); } }
Ve kullanımı şu şekilde:
[Test] public void NBuilderTest() { var invoiceLine = TestDoubles.NBuilder.InvoiceLineBuilder .Default() .With(x => x.ItemCode, "S002") .With(x => x.Vat, 9) .Build(); Assert.IsNotNull(invoiceLine); ToJson(invoiceLine); }
Ve çıktı:
{ "ItemCode": "S002", "UnitCount": 1.0, "UnitPrice": 100.0, "Vat": 9.0, "TotalPrice": 109.00 }
Bogus.Faker'a benzer şekilde, bir sınıf özelliği bir oluşturucu kullanılarak ayarlanmışsa ve bir ayarlayıcısı yoksa değerleri geçersiz kılamazsınız . Böyle bir özellik için With yöntemini kullanmaya çalışırsanız, aşağıdaki istisna ile başarısız olur:
System.ArgumentException : Property set method not found.
EasyTdd.Generators.Builder bir Nuget paketidir ve EasyTdd - Visual Studio Uzantısı ile birlikte çalışır. Bu paket, EasyTdd uzantısı tarafından kullanılan şablonlardan oluşturucular oluşturmak için .NET artımlı kaynak oluşturucuyu kullanır. Oluşturucu oluşturucusu özellik ayarlayıcıları, oluşturucu parametreleri ve her ikisinin bir kombinasyonunu işler. Ayrıca genel parametreleri de destekler.
Bu, bir oluşturucu oluşturmanın benim tercih ettiğim yoludur. Diğer seçeneklere kıyasla avantajları şunlardır:
EasyTdd Visual Studio'ya yüklendiğinde, hedef sınıftaki hızlı eylem menüsünü açın ve "Artımlı Oluşturucu Oluştur" seçeneğini belirleyin:
Bu eylem, BuilderFor öznitelik kümesiyle kısmi bir oluşturucu sınıfı oluşturur:
[EasyTdd.Generators.BuilderFor(typeof(InvoiceLine))] public partial class InvoiceLineBuilder { public static InvoiceLineBuilder Default() { return new InvoiceLineBuilder( () => default, // Set default itemCode value () => default, // Set default unitCount value () => default, // Set default unitPrice value () => default // Set default vat value ); } }
Oluşturucu kodu arka planda üretilir ve bu kısmi sınıf yaygın/uç durum kurulumları için tasarlanmıştır. default
yerine varsayılan değerleri ayarlamakta özgürsünüz.
Kurulumu ve nasıl çalıştığı hakkında daha fazla bilgiyi burada bulabilirsiniz.
İyi tarafı, rastgele değerlere ihtiyacım olduğunda Bogus'u burada kullanabilirim:
public static InvoiceLineBuilder Random() { var f = new Faker(); return new InvoiceLineBuilder( () => f.Random.AlphaNumeric(5), () => f.Random.Decimal(10, 1000), () => f.Random.Number(1, 5), () => f.PickRandom(21, 9, 0) ); }
Kullanım:
[Test] public void EasyTddBuilder() { var invoiceLine = TestDoubles.Builders.InvoiceLineBuilder .Random() .WithUnitPrice(100) .WithUnitCount(1) .Build(); Assert.IsNotNull(invoiceLine); ToJson(invoiceLine); }
Ve çıktı:
{ "ItemCode": "ana0i", "UnitCount": 1.0, "UnitPrice": 100.0, "Vat": 9.0, "TotalPrice": 109.00 }
EasyTdd ayrıca EasyTdd.Generators Nuget paketine bağımlılık olmadan tam builder kodu üretimi sunar. Üçüncü taraf kütüphanelere bağımlı olmak istemiyorsanız veya buna izin verilmiyorsa bu yararlıdır. Uzantı kodu üretir ve her şey projenizdedir, hiçbir bağımlılık veya eklenmiş dize yoktur. Her şeyi değiştirmekten çekinmeyin, hepsi sizin. Bu yaklaşım, hedef sınıf değişiklikleri sırasında otomatik yenileme hariç EasyTdd.Generators durumunun tüm avantajlarını sunar.
Bu durumda, oluşturucunun manuel olarak (birkaç tıklamayla da) yeniden oluşturulması gerekir. Yeniden oluşturma sırasında kurulumların kaybolmasını önlemek için iki dosya oluşturulur. Bir dosya, tüm gerekli yöntemlerle oluşturucu sınıf bildirimini içerir, diğeri yalnızca kurulumlar ve yeniden oluşturma için amaçlanmayan ek yöntemler içindir. Sınıf, yukarıdakine benzer şekilde, hızlı eylem menüsünü açıp "Oluşturucu Oluştur"a tıklayarak oluşturulabilir:
Oluşturucu zaten oluşturulmuşsa araç oluşturucu sınıfını açmayı veya yeniden oluşturmayı önerir:
Bu blog yazısında, builder modelini ve test odaklı geliştirmede kullanımını tanıttım. Ayrıca, manuel uygulamadan başlayarak Bogus.Faker ve NBuilder gibi üçüncü taraf kütüphaneleri, BuilderGenerator ve EasyTdd.Generators.Builder gibi artımlı kod üreteçlerini ve son olarak tüm kodun EasyTdd Visual Studio uzantısı tarafından oluşturulmasını kullanarak bunu uygulamanın birkaç yolunu da gösterdim. Her yöntemin kendine özgü güçlü ve zayıf yönleri vardır ve basit durumlarda iyi çalışır.
Ancak, değişmez sınıflarla uğraşırken, EasyTdd, bir özellik değeri bir ayarlayıcı tarafından mı yoksa bir oluşturucu parametresi aracılığıyla mı başlatılmış olursa olsun, özellik değişikliklerini eşit şekilde ele alarak öne çıkar. EasyTdd şablonları destekler ve çıktıyı tercihlerinize uyacak şekilde özelleştirmenize olanak tanır. EasyTdd ayrıca uygulama hızı nedeniyle bir oluşturucuyu uygulamanın diğer yöntemlerini geride bırakır. Visual Studio'da yalnızca birkaç tıklamayla dosyaları otomatik olarak oluşturmak için araçlar sağlar, zamandan ve emekten tasarruf sağlar.
Unsplash'ta Markus Spiske'nin fotoğrafı