Değişmez bir nesnede argüman doğrulamasını ve alan başlatmayı test etmenin daha kolay bir yolu var mı?


20

Alan adım şöyle basit değişmez sınıflardan oluşur:

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        if (fullName == null)
            throw new ArgumentNullException(nameof(fullName));
        if (nameAtBirth == null)
            throw new ArgumentNullException(nameof(nameAtBirth));
        if (taxId == null)
            throw new ArgumentNullException(nameof(taxId));
        if (phoneNumber == null)
            throw new ArgumentNullException(nameof(phoneNumber));
        if (address == null)
            throw new ArgumentNullException(nameof(address));

        FullName = fullName;
        NameAtBirth = nameAtBirth;
        TaxId = taxId;
        PhoneNumber = phoneNumber;
        Address = address;
    }
}

Boş kontroller ve özellik başlatma yazma zaten çok sıkıcı oluyor ama şu anda argüman doğrulamanın doğru çalıştığını ve tüm özelliklerin başlatıldığını doğrulamak için bu sınıfların her biri için birim testleri yazıyorum . Bu orantısız fayda ile son derece sıkıcı meşgul işi gibi geliyor.

Asıl çözüm, C # 'ın doğal olarak değişmezliği ve null olmayan referans tiplerini desteklemesi olacaktır. Ancak bu arada durumu iyileştirmek için ne yapabilirim? Tüm bu testleri yazmaya değer mi? Her biri için test yazmaktan kaçınmak için bu tür sınıflar için bir kod üreteci yazmak iyi bir fikir olabilir mi?


İşte cevaplara dayanarak sahip olduğum şey.

Ben null denetimleri ve özellik başlatma gibi basitleştirmek için basitleştirmek olabilir:

FullName = fullName.ThrowIfNull(nameof(fullName));
NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
TaxId = taxId.ThrowIfNull(nameof(taxId));
PhoneNumber = phoneNumber.ThrowIfNull(nameof(phoneNumber));
Address = address.ThrowIfNull(nameof(address));

Robert Harvey'in aşağıdaki uygulamasını kullanarak :

public static class ArgumentValidationExtensions
{
    public static T ThrowIfNull<T>(this T o, string paramName) where T : class
    {
        if (o == null)
            throw new ArgumentNullException(paramName);

        return o;
    }
}

Boş çekler test kullanılarak kolayca GuardClauseAssertiondan AutoFixture.Idioms(öneriye, sayesinde Esben Skov Pedersen ):

var fixture = new Fixture().Customize(new AutoMoqCustomization());
var assertion = new GuardClauseAssertion(fixture);
assertion.Verify(typeof(Address).GetConstructors());

Bu daha da sıkıştırılabilir:

typeof(Address).ShouldNotAcceptNullConstructorArguments();

Bu uzantı yöntemini kullanarak:

public static void ShouldNotAcceptNullConstructorArguments(this Type type)
{
    var fixture = new Fixture().Customize(new AutoMoqCustomization());
    var assertion = new GuardClauseAssertion(fixture);

    assertion.Verify(type.GetConstructors());
}

3
Başlık / etiket, nesnenin inşaat sonrası birim testini gerektiren alan değerlerinin (birim) testinden bahseder, ancak kod snippet'i giriş argümanını / parametre doğrulamasını gösterir; bunlar aynı kavramlar değildir.
Erik Eidt

2
Bilginize, bu tür bir ısıtıcı plakayı kolaylaştıracak bir T4 şablonu yazabilirsiniz. (Ayrıca == null
değerinin

1
Otomatik düzeltme deyimlerine bir göz attınız mı? Deyimler
Esben Skov Pedersen


1
Varsayılan izin verme moduna sahip görünmese de Fody / NullGuard'ı da kullanabilirsiniz .
Bob

Yanıtlar:


5

Tam olarak bu tür durumlar için bir t4 şablonu oluşturdum. Değişmez sınıflar için çok sayıda kaynatma plakası yazmaktan kaçınmak için.

https://github.com/xaviergonz/T4Immutable T4Immutable, değişmez sınıflar için kod üreten C # .NET uygulamaları için bir T4 şablonu.

Özellikle null olmayan testler hakkında konuşmak, bunu kullanırsanız:

[PreNotNullCheck, PostNotNullCheck]
public string FirstName { get; }

Yapıcı şu olacaktır:

public Person(string firstName) {
  // pre not null check
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  // assignations + PostConstructor() if needed

  // post not null check
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

Bunu söyledikten sonra, boş denetim için JetBrains Ek Açıklamaları kullanıyorsanız, bunu da yapabilirsiniz:

[JetBrains.Annotations.NotNull, ConstructorParamNotNull]
public string FirstName { get; }

Ve kurucu şu olacak:

public Person([JetBrains.Annotations.NotNull] string firstName) {
  // pre not null check is implied by ConstructorParamNotNull
  if (firstName == null) throw new ArgumentNullException(nameof(firstName));

  FirstName = firstName;
  // + PostConstructor() if needed

  // post not null check implied by JetBrains.Annotations.NotNull on the property
  if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
}

Ayrıca bundan daha fazla özellik var.


Teşekkür ederim. Sadece denedim ve mükemmel çalıştı. Orijinal sorudaki tüm sorunları ele aldığı için bunu kabul edilen cevap olarak işaretliyorum.
Botond Balázs

16

Tüm bu çitleri yazma problemini kolaylaştıracak basit bir yeniden düzenleme ile biraz iyileştirme yapabilirsiniz. İlk olarak, bu uzantı yöntemine ihtiyacınız var:

internal static T ThrowIfNull<T>(this T o, string paramName) where T : class
{
    if (o == null)
        throw new ArgumentNullException(paramName);

    return o;
}

Daha sonra şunları yazabilirsiniz:

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        FullName = fullName.ThrowIfNull(nameof(fullName));
        NameAtBirth = nameAtBirth.ThrowIfNull(nameof(nameAtBirth));
        TaxId = taxId.ThrowIfNull(nameof(taxId));
        PhoneNumber = phoneNumber.ThrowIfNull(nameof(fullName));
        Address = address.ThrowIfNull(nameof(address));
    }
}

Uzantı parametresinde orijinal parametrenin döndürülmesi akıcı bir arayüz oluşturur, böylece isterseniz bu kavramı diğer uzantı yöntemleriyle genişletebilir ve bunları ödevinizde birbirine zincirleyebilirsiniz.

Diğer teknikler kavram açısından daha zariftir, ancak parametreyi bir [NotNull]öznitelikle dekore etmek ve Reflection'ı bu şekilde kullanmak gibi yürütmede giderek daha karmaşıktır .

Bununla birlikte, sınıfınız herkese açık bir API'nin parçası değilse, tüm bu testlere ihtiyacınız olmayabilir.


3
Bunun reddedilmesi garipti. Roslyn'e bağlı olmaması dışında, en çok oylanan cevabın sağladığı yaklaşıma çok benziyor.
Robert Harvey

Bu konuda sevmediğim şey, yapıcıdaki değişikliklere karşı savunmasız olmasıdır.
jpmc26

@ jpmc26: Bu ne anlama geliyor?
Robert Harvey

1
Cevabınızı okuduğumda, yapıcıyı test etmemenizi , ancak bunun yerine her parametrede çağrılan bazı ortak yöntemleri oluşturmanızı ve bunu test etmenizi önerir . Yeni bir özellik / bağımsız değişken çifti ekler ve yapıcıya nullbağımsız değişken üzerinde test etmeyi unutursanız , başarısız bir test elde edemezsiniz.
jpmc26

@ jpmc26: Doğal olarak. Ancak OP'de gösterildiği gibi geleneksel yolla yazarsanız da bu geçerlidir. Tüm ortak yöntem throwyapıcı dışına taşımaktır; gerçekten kurucu içindeki kontrolü başka bir yere aktarmıyorsunuz. Bu sadece bir yardımcı yöntem ve eğer seçerseniz işleri akıcı bir şekilde yapmanın bir yoludur .
Robert Harvey

7

Kısa vadede, bu tür testleri yazmanın sıkıcılığı hakkında yapabileceğiniz çok şey yok. Bununla birlikte, muhtemelen önümüzdeki birkaç ay içinde gerçekleşecek olan C # (v7) 'nin bir sonraki sürümünün bir parçası olarak uygulanması nedeniyle atma ifadeleriyle gelen bazı yardımlar vardır:

public class Person
{
    public string FullName { get; }
    public string NameAtBirth { get; }
    public string TaxId { get; }
    public PhoneNumber PhoneNumber { get; }
    public Address Address { get; }

    public Person(
        string fullName,
        string nameAtBirth,
        string taxId,
        PhoneNumber phoneNumber,
        Address address)
    {
        FullName = fullName ?? throw new ArgumentNullException(nameof(fullName));
        NameAtBirth = nameAtBirth ?? throw new ArgumentNullException(nameof(nameAtBirth));
        TaxId = taxId ?? throw new ArgumentNullException(nameof(taxId)); ;
        PhoneNumber = phoneNumber ?? throw new ArgumentNullException(nameof(phoneNumber)); ;
        Address = address ?? throw new ArgumentNullException(nameof(address)); ;
    }
}

Try Roslyn webapp'ı aracılığıyla atma ifadeleri ile deney yapabilirsiniz .


Çok daha kompakt, teşekkürler. Yarım çizgi kadar. C # 7'yi dört gözle bekliyorum!
Botond Balázs

@ BotondBalázs isterseniz hemen VS15 önizleme deneyebilirsiniz
user1306322

7

Kimsenin NullGuard'dan bahsetmediğine şaşırdım . NuGet aracılığıyla kullanılabilir ve derleme süresi boyunca bu boş kontrolleri IL'ye otomatik olarak örecektir.

Yani yapıcı kodunuz

public Person(
    string fullName,
    string nameAtBirth,
    string taxId,
    PhoneNumber phoneNumber,
    Address address)
{
    FullName = fullName;
    NameAtBirth = nameAtBirth;
    TaxId = taxId;
    PhoneNumber = phoneNumber;
    Address = address;
}

ve NullGuard tam olarak yazdıklarınıza dönüştürmeniz için bu null kontrolleri ekleyecektir.

Bununla birlikte, NullGuard'ın devre dışı bırakıldığını , yani, bu null kontrolleri her yönteme ve yapıcı bağımsız değişkenine, özellik alıcısına ve ayarlayıcıya ekleyeceğini ve öznitelikle açıkça bir null değere izin vermedikçe yöntem dönüş değerlerini kontrol edeceğini unutmayın [AllowNull].


Teşekkür ederim, bu çok güzel bir genel çözüm gibi görünüyor. Her ne kadar varsayılan olarak dil davranışını değiştirmesi çok talihsiz olsa da. [NotNull]Varsayılan olarak null olmasına izin vermemek yerine bir öznitelik ekleyerek çalışsaydı çok daha fazla istiyorum .
Botond Balázs

@ BotondBalázs: PostSharp diğer parametre doğrulama özellikleri ile birlikte bu vardır. Fody aksine aksine ücretsiz değil. Ayrıca, oluşturulan kod PostSharp DLL'leri çağırır, böylece ürün ile birlikte eklemeniz gerekir. Fody kodunu sadece derleme sırasında çalıştırır ve çalışma zamanında gerekli olmaz.
Roman Reiner

@ BotondBalázs: İlk başta NullGuard'ın varsayılan olarak uygulanması biraz garip buldum ama gerçekten mantıklı. Herhangi bir makul kod tabanında null değerine izin verilmesi, null değerinin denetlenmesinden çok daha az yaygın olmalıdır . Eğer varsa gerçekten boş bir yere izin vermek istediğiniz opt-out yaklaşım güçleri onunla eksiksiz olmasını.
Roman Reiner

5
Evet, bu makul ancak En Küçük Şaşkınlık İlkesini ihlal ediyor . Yeni bir programcı projenin NullGuard kullandığını bilmiyorsa, büyük bir sürpriz bekliyorlar! Bu arada, aşağı oyu da anlamıyorum, bu çok yararlı bir cevap.
Botond Balázs

1
@ BotondBalázs / Roman, NullGuard'ın varsayılan olarak çalışma şeklini değiştiren yeni bir açık modu var gibi görünüyor
Kyle W

5

Tüm bu testleri yazmaya değer mi?

Hayır muhtemelen değil. Bunu bertaraf etme ihtimaliniz nedir? Bazı semantiklerin altınızdan değişme olasılığı nedir? Birisi onu mahvederse ne olur?

Nadiren kırılacak bir şey için test yapmak için çok zaman harcıyorsanız ve eğer öyleyse önemsiz bir düzeltmedir ... belki buna değmez.

Her biri için test yazmaktan kaçınmak için bu tür sınıflar için bir kod üreteci yazmak iyi bir fikir olabilir mi?

Olabilir? Bu tür şeyler yansıma ile kolayca yapılabilir. Dikkate alınması gereken bir şey, gerçek kod için kod üretimi yapmaktır, bu yüzden insan hatası olabilecek N sınıfınız yoktur. Hata önleme> hata algılama.


Teşekkür ederim. Belki de soruma yeterince açık değildim, ama onlar için testler değil, değişmez sınıflar üreten bir jeneratör yazmak istedim
Botond Balázs

2
Ben de if...throwonlar yerine, birkaç kez gördüm ThrowHelper.ArgumentNull(myArg, "myArg"), bu şekilde mantık hala orada ve kod kapsama dinged alamadım. ArgumentNullYöntemin nerede yapılacağı if...throw.
Matthew

2

Tüm bu testleri yazmaya değer mi?

Hayır.
Çünkü bu özellikleri, bu sınıfların kullanıldığı diğer bazı mantık testleri aracılığıyla test ettiğinizden eminim.

Örneğin, Fabrika sınıfı için bu özelliklere dayalı onaylama içeren testler yapabilirsiniz (Örneğin, düzgün atanmış Nameözellik ile oluşturulan örnek).

Bu sınıflar, üçüncü bir kısım / son kullanıcı tarafından kullanılan genel API'ya maruz kalıyorsa (@EJoshua fark ettiğiniz için teşekkürler), o zaman beklenen testler ArgumentNullExceptionyararlı olabilir.

C # 7'yi beklerken uzatma yöntemini kullanabilirsiniz

public MyClass(string name)
{
    name.ThrowArgumentNullExceptionIfNull(nameof(name));
}

public static void ThrowArgumentNullExceptionIfNull(this object value, string paramName)
{
    if(value == null)
        throw new ArgumentNullException(paramName);
}

Test nulliçin, her parametre için referans oluşturmak ve beklenen istisna için iddia oluşturmak üzere yansıma kullanan bir parametreli test yöntemi kullanabilirsiniz .


1
Bu, mülkün başlatılmasını test etmemek için inandırıcı bir argüman.
Botond Balázs

Bu, kodu nasıl kullandığınıza bağlıdır. Kod halka açık bir kütüphanenin parçasıysa, kitaplığınızı nasıl yapacağını bilmek için başkalarına asla körü körüne güvenmemelisiniz (özellikle kaynak kodlara erişimleri yoksa); ancak, kendi kodunuzu çalıştırmak için dahili olarak kullandığınız bir şey varsa, kendi kodunuzu nasıl kullanacağınızı bilmelisiniz, bu nedenle kontrollerin amacı daha azdır.
EJoshuaS - Monica

2

Her zaman aşağıdaki gibi bir yöntem yazabilirsiniz:

// Just to illustrate how to call this
private static void SomeMethod(string a, string b, string c, string d)
    {
        ValidateArguments(a, b, c, d);
        // ...
    }

    // This is the one to use as a utility function
    private static void ValidateArguments(params object[] args)
    {
        for (int i = 0; i < args.Length; i++)
        {
            if (args[i] == null)
            {
                StackTrace trace = new StackTrace();
                // Get the method that called us
                MethodBase info = trace.GetFrame(1).GetMethod();

                // Get information on the parameter that is null so we can add its name to the exception
                ParameterInfo param = info.GetParameters()[i];

                // Raise the exception on behalf of the caller
                throw new ArgumentNullException(param.Name);
            }
        }
    }

Bu tür bir doğrulama gerektiren birkaç yönteminiz varsa, en azından, bu size biraz tasarruf sağlar. Tabii ki, bu çözüm yönteminizin parametrelerinin hiçbirinin olamayacağını varsayar null, ancak isterseniz bunu değiştirmek için bunu değiştirebilirsiniz.

Bunu, türe özgü başka bir doğrulama gerçekleştirmek için de genişletebilirsiniz. Örneğin, dizelerin tamamen boşluk veya boş olamayacağı bir kuralınız varsa, aşağıdaki koşulu ekleyebilirsiniz:

// Note that we already know based on the previous condition that args[i] is not null
else if (args[i].GetType() == typeof(string))
            {
                string argCast = arg as string;

                if (!argCast.Trim().Any())
                {
                    ParameterInfo param = GetParameterInfo(i);

                    throw new ArgumentException(param.Name + " is empty or consists only of whitespace");
                }
            }

Temelde atıfta bulunduğum GetParameterInfo yöntemi yansımayı yapar (böylece aynı şeyi tekrar tekrar yazmaya gerek kalmaz, bu da DRY ilkesinin ihlali anlamına gelir ):

private static ParameterInfo GetParameterInfo(int index)
    {
        StackTrace trace = new StackTrace();

        // Note that we have to go 2 methods back to get the ValidateArguments method's caller
        MethodBase info = trace.GetFrame(2).GetMethod();

        // Get information on the parameter that is null so we can add its name to the exception
        ParameterInfo param = info.GetParameters()[index];

        return param;
    }

1
Bu neden reddediliyor? Çok kötü değil
Gaspa79

0

Yapıcıdan istisnalar atmayı önermiyorum. Sorun şu ki, testiniz için ilgisiz olsalar bile geçerli parametreleri geçmek zorunda olduğunuz için bunu test etmekte zorlanacaksınız.

Örneğin: Üçüncü parametrenin bir istisna verip vermediğini test etmek istiyorsanız, birinci ve ikinciyi doğru bir şekilde geçmeniz gerekir. Yani testiniz artık izole değil. birinci ve ikinci parametrenin kısıtlamaları değiştiğinde, bu test durumu üçüncü parametreyi test etse bile başarısız olur.

Java doğrulama API'sını kullanmanızı ve doğrulama işlemini harici hale getirmenizi öneririm. Dört sorumluluk (sınıf) içermesini öneriyorum:

  1. Java doğrulaması olan bir öneri nesnesi, bir ConstraintViolations Kümesi döndüren bir validate yöntemiyle açıklama eklenmiş parametreler. Bir avantaj, geçerli olduğu iddiası olmadan bu nesnelerin etrafından geçebilmenizdir. Etki alanı nesnesini somutlaştırmayı denemeden, gerekli olana kadar doğrulamayı geciktirebilirsiniz. Doğrulama nesnesi, katmana özel bilgisi olmayan bir POJO olduğu için farklı katmanlarda kullanılabilir. Herkese açık API'nızın bir parçası olabilir.

  2. Geçerli nesne oluşturmaktan sorumlu olan etki alanı nesnesi için bir fabrika. Öneri nesnesini iletirsiniz ve fabrika doğrulamayı çağırır ve her şey yolundaysa etki alanı nesnesini oluşturur.

  3. Diğer geliştiriciler için örnekleme için çoğunlukla erişilememesi gereken etki alanı nesnesinin kendisi. Test amacıyla bu sınıfın paket kapsamında olmasını öneririm.

  4. Somut etki alanı nesnesini gizlemek ve kötüye kullanımı zorlaştırmak için bir kamu malı arayüzü.

Boş değerleri kontrol etmenizi önermiyorum. Etki alanı katmanına girer girmez, uçları olan nesne zincirlerine veya tek nesneler için arama işlevlerine sahip olmadığınız sürece, null değerinden veya null döndürmekten kurtulursunuz.

Diğer bir nokta: mümkün olduğunca az kod yazmak, kod kalitesi için bir ölçüt değildir. 32k oyun yarışmalarını düşünün. Bu kod, yalnızca teknik sorunları önemsediği ve anlambilimi önemsemediği için sahip olabileceğiniz en kompakt ancak aynı zamanda en karışık koddur. Fakat anlambilim, işleri kapsamlı kılan noktalardır.


1
Son noktanıza saygıyla katılmıyorum. 100 kez aynı klişe yazmak zorunda değil etmez yardım bir hata yapma olasılığını azaltmak. Bunun Java olup olmadığını düşünün, C # değil. Her özellik için bir destek alanı ve bir getter yöntemi tanımlamak gerekir. Kod tamamen okunamaz hale gelecek ve önemli olan, karmaşık altyapı tarafından gizlenecektir.
Botond Balázs
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.