Qual è una buona alternativa al modello TestDataBuilder?

2

Il modello TestDataBuilder viene utilizzato nel test delle unità per creare collaboratori. I suoi vantaggi rispetto all'uso dei costruttori:

  • Le modifiche ai costruttori sono localizzate.
  • Il codice di prova diventa più chiaro, perché non devi specificare tutti i parametri richiesti al costruttore.

Uno svantaggio è che l'utilizzo è alquanto lento e diminuisce la leggibilità:

var service = serviceBuilder.WithRepo(
    repoBuilder.WithEntities(
      new [] { entityBuilder.WithName("name") }).Build())
  .WithMailSender(mailSenderBuilder.Build())
  .Build();
  • La parola "Con" viene in genere ripetuta molte volte.
  • Lo stesso per la chiamata a Build() .
  • Lo stesso per la parola Builder .

Sebbene tutti possano essere mitigati, suppongo che le soluzioni stesse abbiano degli svantaggi. Ad esempio, potremmo rinominare serviceBuilder in service , ma ciò potrebbe dare l'impressione sbagliata sul tipo di variabile. Potremmo implementare una conversione implicita da una classe builder all'oggetto che genera, ma è più codice nel builder in aggiunta ai già abbondanti metodi With* .

Esiste un'alternativa a TestDataBuilder che non ha questi svantaggi?

    
posta Gebb 10.06.2015 - 17:25
fonte

2 risposte

1

Il mio suggerimento è quello di utilizzare il modello di fabbrica con una svolta, sfruttando i parametri predefiniti. L'implementazione è più concisa di quella di un builder, anche il codice di test.

Lo chiamo MotherFactory . Ecco il codice:

// ==== In a common test library ===

public abstract class MotherFactory
{
}


// ==== In a test project ===

public static class MotherFactoryEntityExtensions
{
    public static Entity Entity(
        this MotherFactory a,
        int? year = 2014,
        string name = "name",
        Dependency dependency = null,
        withoutDependency = false)
    {
        if (withoutDependency && dependency != null)
        {
            throw new ArgumentException(
                "The parameter 'dependency' cannot be used when true is specified for 'withoutDependency'.",
                "dependency");
        }

        if(dependency == null && !withoutDependency)
        {
            dependency = a.Dependency();
        }
        return new Entity(startYear, endYear, name, summary, dependency);
    }
}

public class EntityTest
{
    private static readonly MotherFactory an = null;

    [Fact]
    public void Ctor_GivenNullDependency_Throws()
    {
        Exception e = Record.Exception(() =>
        {
            Entity systemUnderTest = an.Entity(withoutDependency: true);
        });

        Assert.NotNull(e);
    }
}

public class ServiceTest
{
    private static readonly MotherFactory a = null;
    private static readonly MotherFactory an = null;

    [Fact]
    public void Find_GivenCorrectRequest_FindsEntity()
    {
        var service = a.Service(repo: a.Repository(dbEntities: new [] { an.Entity(name: "XXX") });
        var result = service.Find();
        Assert.Equal("XXX", result.Name);
    }
}

Note

  • Inserisci la classe MotherFactory in una libreria e nei progetti di test crea una classe con metodi di estensione per ogni classe di collaboratore.
  • I parametri che hanno un prefisso without (come withoutDependency ) vengono utilizzati solo nei casi in cui vogliamo disabilitare la creazione dell'istanza predefinita di una dipendenza e passare invece null .
  • Una classe MotherFactory*Extensions può anche essere una ObjectMother fornendo metodi come UserWithCompleteProfile() .
risposta data 10.06.2015 - 17:25
fonte
4

Anche se spero di poter rispondere a parti di questa domanda in modo utile, do sento la necessità di mettere in discussione parti della premessa.

  • Innanzitutto, la ripetizione di With[...] e Build() non deve essere un problema. Dopo tutto, la produttività dello sviluppo del software è appena misurata dalla velocità con cui è possibile digitare il codice, e da una prospettiva di lettura, la ripetizione può a volte aiutare a capire, perché aggiunge una struttura al codice.
  • In secondo luogo, mentre mi rendo conto che l'esempio sopra riportato è solo questo: un esempio, non penso che sia male avere un paio di% step% co_de. Se, d'altra parte, si dispone di un sacco di tale codice, potrebbe non indicare un problema con il modello Test Data Builder stesso, ma potrebbe essere un odore che il test in questione è troppo grossolana, o il tentativo di prova troppo.

A seconda della tua lingua, puoi usare vari trucchi. In C #, ad esempio, puoi evitare la necessità del metodo Build aggiungendo conversioni esplicite o implicite tra i Builder e le loro classi di destinazione.

Tuttavia, penso che una migliore API SUT possa risolvere i problemi in un modo migliore. Ad esempio, considerando l'esempio precedente, potresti progettare il SUT in modo che tu possa semplicemente scrivere il codice in questo modo:

var service = defaultService
    .WithRepo(defaultRepo.WithEntities(
        new [] { defaultEntity.WithName("name") })
    .WithMailSender(defaultMailSender);

Cioè, la stessa API SUT ha metodi Build() , mentre gli oggetti With[...] sono oggetti predefiniti specifici del test.

Notare, tuttavia, che questo particolare esempio dice default[...] , che mi aspetterei di essere ridondante, in modo che l'esempio possa probabilmente essere ulteriormente ridotto:

var service = defaultService
    .WithRepo(defaultRepo.WithEntities(
        new [] { defaultEntity.WithName("name") });

Ho delineato varie opzioni in un articolo serie sull'argomento .

    
risposta data 13.09.2017 - 19:12
fonte

Leggi altre domande sui tag