UnitTests, ripulire la tua dichiarazione di attualità rende il tuo test più o meno chiaro?

4

Diciamo che stiamo testando FooClass con il seguente metodo:

    public void Foo(string stringParameter, int intParameter, Action<Bar> successCallback, Action<Exception> errorCallback);

Se la chiamata a Foo ha esito positivo, il successCallback verrà chiamato con il risultato di Foo nella forma di un oggetto Bar . Se fallisce, il errorCallback verrà chiamato con Exception .

Quindi i test avranno un aspetto simile a questo:

[TestMethod]
public void Foo_UnderGivenConditions_WeExpectAGivenResult()
{
    //Arrange
    var fooObject = CreateFooObjectWithGivenConditions( ... );

    //Act
    fooObject.Foo(String.Empty, 0, (bar) => { ... }, (error) => { ... });

    //Assert
    Assert.AreEqual(..., ...);
}

Ora ci sono molti test su questo Foo -method, tutti contenenti una chiamata a Foo , ma non tutti i test si prenderanno cura di tutti i parametri. Alcuni possono fornire valori di stringa diversi, ma non si preoccupano del parametro int, e alcuni dovranno fornire un callback di errore per affermare che viene generata l'eccezione giusta e così via.

Ora, poiché tutti i parametri sono obbligatori, dobbiamo passare un valore stringa al metodo Foo , anche se il valore non ha significato per il test. Ci sarà un sacco di "I don't care" o "some text" . Quando arriva qualcun altro e legge il test, deve considerare se il valore dato ha effettivamente un significato per il risultato del test o meno. Lo stesso vale per i callback. A volte abbiamo bisogno dei callback per ottenere il valore del risultato o l'eccezione, ma il più delle volte no.

Quindi, implementa un metodo di estensione, PerformFoo , che imposta automaticamente tutti i parametri:

public static Bar PerformBar(this FooClass fooObject, string stringParameter = "some text", int intParameter = 0, Action<Bar> successCallback = null, Action<Exception> errorCallback = null);
{
   Bar result = null;

   var ourCallback = (bar) => 
      { 
          result = bar; 
          if (successCallback != null) 
              sucessCallback.invoke();
      }

   fooObject.Foo(stringParameter, intParameter, ourCallback, errorCallback);

   return result;
}

Il metodo di estensione chiamerà Foo con i parametri predefiniti e restituirà anche il risultato se viene chiamato successCallback. Questo ci permette di cambiare la parte del test Act in qualcosa del tipo:

//Act, all we care about is the string parameter:
fooObject.PerformFoo("A string that we care about");

//Act, we need the resulting bar when the int parameter is 10:
var bar = fooObject.PerformFoo(intParameter: 10);

//Act, we still needs to provide a callback to get the exception 
fooObject.PerformFoo("SomeInvalidValueCausingAnException", errorCallback: (error) => { exceptionThrown = error; }); 

Quindi le domande sarebbero:

  • Questo rende i test più leggibili?
  • È più facile ottenere ciò che il test verifica realmente?
  • Il fatto che chiamiamo PerformFoo , che non esiste realmente nella classe in esame, rende il test meno utile come documentazione?
  • Abbandona il metodo di estensione per un metodo normale prendendo fooObject come primo parametro meno di una "bugia"? (ad esempio PerformFoo(fooObject, intParameter: 10) )

Quanto lontano farai per rendere i tuoi test puliti e chiari?

    
posta Vegar 11.08.2011 - 21:03
fonte

3 risposte

1
//Act, we need the resulting bar when the int parameter is 10:
var bar = fooObject.PerformFoo(intParameter: 10);

Quando uno sviluppatore incontra questo test unitario, mi ci vorranno alcuni secondi per andare oltre quel metodo "proxy" e capire quale comportamento della classe sottoposta a test viene effettivamente testato, poiché non vi è alcuna menzione diretta del metodo originale Quindi non direi che è più leggibile di un test che usa la cosa reale. Questo ulteriore livello è anche soggetto a errori. A mio parere, il metodo utilizzato nella sezione Act dovrebbe sempre essere lo stesso metodo che vogliamo testare.

Nel tuo esempio originale, usi variabili anonime per rendere i parametri irrilevanti più trasparenti, penso che sia il modo ragionevole per semplificare i test, come

fooObject.Foo("", 0, (bar)=>{}, (bar)=>{});
    
risposta data 30.08.2011 - 15:24
fonte
0

Se lo facessi, darei un'occhiata all'impostazione di base che agisce chiamando Foo() . Una classe base conterrà l'Arrange con valori predefiniti, l'Act sarà nella base e l'assert dovrebbe essere assunto nelle classi di test figlio.

[TestClass] //you don't need this here but the 
            //compiler warns otherwise with mstest :(
public void Base_Fooey_fixture
{
   protected String StringParameter { get; set; }
   protected int IntParameter { get; set; }
   // ...
   [ClassInitialize]
   public void Arrange()
   {
     StringParameter = "Default Value";
     //...
   }
   [TestInitialize]
   public void Act()
   {
    Foo(StringParameter, IntParameter, SuccessCallback, ErrorCallback);  
   }
}
[TestClass]
public class RealTest : Base_Fooey_fixture
{
   [ClassInitialize]
   public class MyArrange()
   {
     StringParameter = "My real string value";
   }

   [TestMethod]
   public void Im_gonna_assert_for_this_setup()
   {
      //...
   }
}
    
risposta data 14.08.2011 - 15:50
fonte
0

Non sono sicuro che l'utilizzo dei callback per l'asserzione di eccezioni sia meglio dell'attributo [ExpectedException].

Per un codice di prova, non è diverso da qualsiasi altro codice in cui vuoi che sia chiaro.

Nei miei test, di solito applico i principi di programmazione al codice di test che trovo produttivo senza esitazione.

La composizione è un potente strumento per formare un linguaggio ubiquo specifico per il modello di dominio, ma a mio gusto, il metodo PerformBar è troppo generico. Di solito finisco con metodi di composizione più specializzati.

public class RandomQueueGeneratesSequence
{
 private IEnumerable<T> ActualResult<T>(IEnumerable<T> source, int seed)
 {
  // Setup
  var q = new RandomQueue(new Random(seed));

  // Act
  foreach(var i in source)
    q.Add(i);
  var l = new List<T>();
  while( q.HasItems )
  {
   l.Add(q.GetItem());
  }
  return l;
 }
 [Test, Combinatorial] 
 public void ContainingAllSourceItems([Values(0,1,12345)] int seed)
 {
  CollectionAssert.AreEqualent(new[]{1,2,3}, ActualResult(new[]{1,2,3},seed));
 }
 [Test, Combinatorial]
 public void WithSingleItem([Values(0,1)] expectedValue, [Values(0,1,12345)] int seed)
 {
  Assert.AreEqual(expectedValue, ActualResult(new[]{expectedValue}, seed).Single());
 }
 [Test]
 public void InOrderBySeed1()
 {
  CollectionAssert.AreEqual(new[]{1,2}, ActualResult(new[]{1,2}, 123));
 }
 [Test]
 public void InOrderBySeed2()
 {
  CollectionAssert.AreEqual(new[]{2,1}, ActualResult(new[]{1,2}, 456));
 }
}

In questo esempio, non è ragionevole comporre i metodi di asserzione, perché vengono utilizzati metodi diversi (Assert.AreEqual, CollectionAssert.AreEqual, CollectionAssert.AreEqualent).

Ma se ci sono più di due test segui un pattern, trovato in InOrderBySeed, sposta un assert a un metodo di composizione e assegnagli un Assert :

 protected void AssertInOrder<T>(IEnumerable<T> expected, IEnumerable<T> source, int seed)
 {
  CollectionAssert.AreEqual(expected, ActualResult(source, seed));
 }

 public void InOrderBySeed1()
 {
  AssertInOrder(new[]{1,2}, new {1,2}, 123);
 }
 [Test]
 public void InOrderBySeed2()
 {
  AssertInOrder(new[]{2,1}, new[]{1,2}, 456);
 }
 [Test]
 public void InOrderBySeed3()
 {
  AssertInOrder(new[]{3,1,2}, new[]{1,2,3}, 789);
 }

Generalmente, quando il codice del test diventa abbastanza chiaro, ti ritroverai a non aver più bisogno di commenti. Soprattutto quando l'intento è chiaro da un nome di metodo. In questo esempio - il prefisso Assert annulla il commento // Assert .

    
risposta data 30.08.2011 - 13:16
fonte

Leggi altre domande sui tag