.
Anmeldung | Registrieren | Hilfe | Posteingang
Suchen
Home Foren News Member Offers Termine Developer Blogs Knowledge Base

Navigation

Navigationslinks überspringen.
Knowledge Base reduzierenKnowledge Base
Tutorials reduzierenTutorials
Webentwicklung
Cliententwicklung
Datenbankentwicklung
IT Professional
Sharepoint
Sprachspezifisch reduzierenSprachspezifisch
C#
Visual Basic
C++
XAML
SQL
JavaScript
Erfahrungsberichte reduzierenErfahrungsberichte
Entwicklersoftware
Bücher
FAQ Grundlagen

Verknüpfungen

  • Knowledge Base durchsuchen
  • Hilfe zur Knowledge Base
  • RSS Feed
  • Twitter

Mocks und Stubs im Einsatz (TDD Part 2)

2. Mocks

2.1 Das Problem

In Teil 1 wurden ihnen die Grundlagen für die testgetriebene Entwicklung gezeigt. Im Verlauf sind einige Stellen aufgetaucht, die so ohne weiteres nicht getestet werden können:

/// <summary>
/// Connects to the local business database.
/// </summary>
internal static class Service
{
   public static IEnumerable<Invoice> GetInvoices()
   {
      throw new NotImplementedException();
   }
}

public class Invoice
 {
    // Code ohne Bezug zu diesem Absatz ausgeblendet.

    public void SendToCollection()
    {
       throw new NotImplementedException();
    }
 }

internal class WcfHttpCollectionAgencyService : ICollectionAgencyService
{   
   public Invoice GetInvoice(string invoiceNumber)
   {
      throw new NotImplementedException();
   }

   public bool SendInvoice(Invoice invoice)
   {
      throw new NotImplementedException();
   }
}

 

Alle diese Methoden haben eines gemeinsam: Sie müssen auf eine andere Komponente zugreifen. Hier sind es Servicekomponenten. Ein Zugriff darauf ist nicht möglich, weil diese noch gar nicht existieren. Selbst wenn, so ist es nicht ratsam diese zu benutzen. Das Inkassobüro würde ihnen auf die Füße steigen wenn sie Dummy-Daten in ihre Datenbank schieben!
Was nun?

2.2 Die Lösung

Mocks sind Simulationen von externen Komponenten. Immer dort wo eine externe Komponente referenziert werden muß, wird der Code auf ein Mock umgeleitet. Verwechseln sie nicht Mocks mit Stubs! Letztere haben ein statisches Verhalten. Ein Stub ist zum Beispiel eine Liste mit ein paar Kundenobjekten, die als Datenbank-Ersatz dient. Sie können für die Validierung von Objekten herangezogen werden. Mocks hingegen sind dynamisch. Mit ihnen wird das Verhalten einer echten Komponente in allen Punkten simuliert, die für den Test relevant sind. Was es dabei zu beachten gilt und was best practice ist, darüber haben andere schon ausführlich geschrieben. Ich möchte ihnen daher folgende Artikel als Pflichtlektüre ans Herz legen:

Mock Objects and Stubs: The Bottle Brush of TDD (von Jeremy D. Miller) Lesen sie am besten auch gleich die Folgeartikel von ihm.

Qualities of a Good Unit Test (von Jeremy D. Miller)

Mocks aren't Stubs (von Martin Fowler)

Mit diesen Erkenntnissen können sie nun die oben gezeigten Probleme lösen.
Ich verwende in diesem Artikel das Moq-Framework.
Auf gehts!

2.3 Mocks im praktischen Einsatz

Hier noch einmal das Szenario:

var overDue = new OverDueSpecification();
var noticeSent = new NoticeSentSpecification();
var inCollection = new InCollectionSpecification();

ISpecification<Invoice> sendToCollection =
   overDue.And(noticeSent).And(inCollection.Not());

IEnumerable<Invoice> invoiceCollection = Service.GetInvoices();

foreach(Invoice currentInvoice in invoiceCollection)
{
   if(sendToCollection.IsSatisfiedBy(currentInvoice))
   {
      currentInvoice.SendToCollection();
   }
}

Im Verlauf werden sie feststellen dass es hier ein paar Architekturfehler gibt. Durch die Tests werden sie aufgedeckt, aber darauf komme ich zu gegebener Zeit zurück.

Service.GetInvoices() ist ein typischer Kandidat für ein Stub. Ich habe dafür mal folgendes vorbereitet:

using System;
using System.Collections.Generic;

namespace SpecificationPatternWikipedia.Tests.Stubs
{
   internal static class InvoiceStubsAndFakes
   {
      static InvoiceStubsAndFakes()
      {
         TempInvoiceRepository = new List<Invoice>();
         /* Prepare the TempInvoiceRepo.
         * Invoice "65432" should already be there. */
         foreach(var invoice in BadInvoicesAtCollectionAgency)
         {
            AddToTempInvoiceRepository(invoice);
         }
      }

      /// <summary>
      /// Gets a list of 3 different fake invoices.
      /// ACME corp is never over due and has 0 notices sent.
      /// Invoice number is "12345". 
      /// Bad Guys Inc is over due and they got 3 notices sent.
      /// Invoive number is "98765". 
      /// Evil guys is over due, has 3 notices sent and the collection agency
      /// has a copy of their invoice.
      /// Invoive number is "65432". 
      /// </summary>
      internal static List<Invoice> AllInvoices
      {
         get
         {
            return new List<Invoice>
                      {
                         new Invoice
                            {
                               Customer = new Customer
                                             {
                                                Name = "ACME corp.",
                                                AmountOfNoticesSent = 0
                                             },
                               DueDate = DateTime.Now,
                               InvoiceNumber = "12345",
                               AtCollectionAgency = false
                            },
                         new Invoice
                            {
                               Customer = new Customer
                                             {
                                                Name = "Bad Guys Inc",
                                                AmountOfNoticesSent = 3
                                             },
                               DueDate = new DateTime(2010, 6, 23),
                               InvoiceNumber = "98765",
                               // Not yet at the collection agency.
                               AtCollectionAgency = false
                            },
                         new Invoice
                            {
                               Customer = new Customer
                                             {
                                                Name = "Evil Guys",
                                                AmountOfNoticesSent = 3
                                             },
                               DueDate = new DateTime(2010, 6, 23),
                               InvoiceNumber = "65432",
                               AtCollectionAgency = true
                            }
                      };
         }
      }

      internal static List<Invoice> GoodInvoices
      {
         get
         {
            return new List<Invoice>
                      {
                         AllInvoices.Find(invoice =>
                                          invoice.InvoiceNumber == "12345")
                      };
         }
      }

      internal static List<Invoice> BadInvoicesNotAtCollectionAgency
      {
         get
         {
            return new List<Invoice>
                      {
                         AllInvoices.Find(invoice =>
                                          invoice.InvoiceNumber == "98765")
                      };
         }
      }

      internal static List<Invoice> BadInvoicesAtCollectionAgency
      {
         get
         {
            return new List<Invoice>
                      {
                         AllInvoices.Find(invoice =>
                                          invoice.InvoiceNumber == "65432")
                      };
         }
      }

      internal static List<Invoice> TempInvoiceRepository { get; private set; }

      internal static void AddToTempInvoiceRepository(Invoice invoice)
      {
         if (!TempInvoiceRepository.Contains(invoice))
            TempInvoiceRepository.Add(invoice);
      }
   }
}

Im ersten Anlauf wird tatsächlich nur ein Bruchteil verwendet. Wenn sie die oben erwähnten Artikel gelesen haben, wissen sie dass man exzessives Mocking vermeiden sollte. Um an meine Testdaten zu gelangen brauche ich nicht unbedingt einen Mock für diesen Service, denn dieser ist statisch. Wir haben gelernt dass dafür Stubs verwendet werden können.

Als nächstes schauen sie sich die InCollectionSpecification an:

internal class InCollectionSpecification : ISpecification<Invoice>
{
   #region ISpecification<Invoice> Members

   /// <summary>
   /// Is satisfied when an invoice has already been sent to the collection agency.
   /// </summary>
   public bool IsSatisfiedBy(Invoice entity)
   {
      /* Question: Soll der Customer ein Property erhalten,
       * das angibt ob die collection agency benachrichtigt wurde,
       * oder soll ich ein CollectionAgency-Objekt machen,
       * oder eine Serviceverbindung mit einem Request?
       * Ich entscheide mich für einen Service-Request.*/

      ICollectionAgencyService collectionAgencyService = new WcfHttpCollectionAgencyService();
      Invoice tempInvoice = collectionAgencyService.GetInvoice(entity.InvoiceNumber);
      return tempInvoice != null;
   }

   #endregion ISpecification<Invoice> Members
}

Sie sehen hier gleich den ersten Architekturfehler. Innerhalb einer Methode wird eine externe Komponente referenziert! Das ist ein absolutes NO GO. So etwas ließe sich auch nicht mocken. IsSatisfiedBy stellt intern eine Serviceanfrage für eine bestimmte Rechnung. collectionAgencyService.GetInvoice(entity.InvoiceNumber) muss unbedingt durch eine Simulation ersetzt werden! Es muss also ein Weg geschaffen werden, mit dem dieser Teil gegen ein Mock ausgetauscht werden kann. Damit dieses Problem gelöst werden kann, ist es jetzt an der Zeit sich mit dem "Werkzeugkasten" des Mocking-Frameworks auseinander zu setzen.

2.4 Mocks mit Moq

Mocking-Frameworks arbeiten in der Regel nach folgendem Paradigma:

  • Mock-Instanz erstellen (geschieht mit Hilfe eines Interface oder einer abstrakten Klasse).
  • Verhalten des Mocks beschreiben
  • Mock benutzen
  • Mock-Verhalten verifizieren.

Den ersten Test bei dem Mocks eingesetzt werden, habe ich so geschrieben:

   1: [TestMethod]
   2: public void ShouldCallGetInvoiceFromCollectionAgencyService()
   3: {
   4:    // Arrange
   5:    var collectionAgencyServiceMock =
   6:       new Mock<ICollectionAgencyService>();
   7:    collectionAgencyServiceMock.Setup(m => m.GetInvoice(It.IsAny<String>()))
   8:       .Verifiable("GetInvoice has not been called.");
   9:  
  10:    var inCollectionSpecification = new InCollectionSpecification(
  11:       collectionAgencyServiceMock.Object);
  12:  
  13:    var invoice = new Invoice { InvoiceNumber = "12345" };
  14:    // Act
  15:    inCollectionSpecification.IsSatisfiedBy(invoice);
  16:    // Assert
  17:    collectionAgencyServiceMock.Verify();
  18: }

Zeilen 5-6:
Hier wird das Mock-Objekt gebildet. Das Interface ICollectionAgencyService ist alles was dafür benötigt wird.

Zeilen 7-8:
Das Mock-Objekt bekommt sein Verhalten "einprogrammiert". Auf deutsch übersetzt steht dort: Achte darauf dass GetInvoice aufgerufen wird. Welchen Wert sie als Parameter bekommt, ist egal; Hauptsache es ist ein String.

Zeilen 10-11: 
Hier instanziiere ich die InCollectionSpecification -Klasse und das ist der einzig logische Punkt für die Übergabe des Mock-Objekts!

Zeile 13: Eine beliebige Rechnung wird generiert.

Zeile 15:
Hier wird nun die Methode inCollectionSpecification.IsSatisfiedBy aufgerufen. Der Test soll sicherstellen dass intern auch GetInvoice aufgerufen wird - mehr nicht! Genau das wird in Zeile 17 gemacht.

Diese Testmethode prüft also nicht irgendwelche Rückgabe-Ergebnisse (Status), sondern nur das Verhalten (Behavior)!

Nachdem klar ist, wie der Architekturfehler behoben wird, kann die InCollectionSpecification-Klasse umgeschrieben werden:

internal class InCollectionSpecification : ISpecification<Invoice>
{
   private readonly ICollectionAgencyService collectionAgencyService;

   internal InCollectionSpecification(
      ICollectionAgencyService collectionAgencyService)
   {
      this.collectionAgencyService = collectionAgencyService;
   }

   #region ISpecification<Invoice> Members

   /// <summary>
   /// Is satisfied when an invoice has already been sent to
   ///  the collection agency.
   /// </summary>
   public bool IsSatisfiedBy(Invoice entity)
   {
      // Nicht sicher:
      //bool result = entity.AtCollectionAgency;
      //return result;
      
      // Sicher:
      var tempInvoice =
         collectionAgencyService.GetInvoice(entity.InvoiceNumber);
      return tempInvoice != null;
   }

   #endregion ISpecification<Invoice> Members
}

Jetzt wird eine konkrete Service-Implementierung über den Konstruktor der Klasse injiziert (Dependency Injection).

Architektur-Exkurs
Ich habe mir schon im ersten Teil die Frage gestellt, was denn für die Validierung besser geeignet sei: ein Property in der Invoice-Klasse, welches angibt ob die Rechnung bereits dem Inkassobüro vorliegt, oder eine Service-Anfrage. Mir kam der Gedanke dass es unter Umständen möglich ist dass dieses Property zwar auf true steht (liegt vor), das Inkassobüro aber in Wahrheit gar keine Kenntniss von dieser Rechnung hat. Um hier Sicherheit zu erlangen, habe ich die Service-Verbindung vorgezogen. Das kann je nach Netz- und Serverauslastung Performance-Einbußen bringen, aber ich denke und hoffe doch dass ihre Firma nicht mit großen Mengen von säumigen Schuldnern konfrontiert ist. ;-)

Nun möchte ich aber doch ganz gerne wissen ob die inCollectionSpecification.IsSatisfiedBy(invoice) Methode auch das macht was sie soll. Was soll sie denn? Nochmal zur Erinnerung, die Anforderung lautet: Die Spezifikation ist erfüllt wenn die angegebene Rechnung dem Inkassobüro vorliegt.
Dafür schreibe ich folgende Testmethode:

   1: [TestMethod]
   2: public void InCollectionSpecificationShouldBeSatisfied()
   3: {
   4:    // Arrange
   5:    var invoice = new Invoice { InvoiceNumber = "65432" };
   6:    var collectionAgencyServiceMock =
   7:       new Mock<ICollectionAgencyService>();
   8:  
   9:    collectionAgencyServiceMock.Setup(collectionAgencyService =>
  10:       collectionAgencyService.GetInvoice(It.IsAny<string>()))
  11:       .Returns((string invoiceNumber) =>
  12:          InvoiceStubsAndFakes.BadInvoicesAtCollectionAgency
  13:          .Find(tempInvoice =>
  14:             tempInvoice.InvoiceNumber == invoiceNumber));
  15:  
  16:    var inCollectionSpecification =
  17:       new InCollectionSpecification(collectionAgencyServiceMock.Object);
  18:    // Act
  19:    bool isAtCollectionAgency =
  20:       inCollectionSpecification.IsSatisfiedBy(invoice);
  21:    // Assert
  22:    Assert.IsTrue(isAtCollectionAgency);
  23: }

Zeile 5:
Eine Rechnung mit der Nummer "65432" wird generiert. Diese entspricht einer Rechnung, die laut meinem Stub (siehe oben) schon bei dem Inkassobüro vorliegt.

Zeilen 6-7:
Das Mock für den CollectionAgencyService wird erzeugt.

Zeilen 9-10:
Das Mock wird angewiesen, jeden String anzunehmen.

Zeilen 11-14:
Hier wird die komplette Funktionalität simuliert.

Zeilen 16-17: 
Das Mock wird in die InCollectionSpecification injiziert.

Zeilen 19-20:
Die Simulation wird gefahren.

Schön und gut, bis hier läuft ja alles. Was aber noch nicht getestet wurde, ist das komplette Szenario (siehe erstes Listing in Kapitel 2.3). Das möchte ich hier zum Schluß nachholen.

[TestMethod]
public void PatternTest()
{
   // Arrange
   var collectionAgencyService = new Mock<ICollectionAgencyService>();
   SetupGetInvoiceMock(collectionAgencyService);
   SetupSendToCollectionMock(collectionAgencyService);
   Mock<IRepository> repository = MockRepositoryGetAllInvoices();
   
   // Pattern Setup
   var overDue = new OverDueSpecification();
   var noticeSent = new NoticeSentSpecification();
   var inCollection =
      new InCollectionSpecification(collectionAgencyService.Object);
   ISpecification<Invoice> sendToCollection =
      overDue.And(noticeSent).And(inCollection.Not());
   IEnumerable<Invoice> invoiceCollection =
      Service.GetInvoices(repository.Object);
   
   // Act
   foreach (Invoice currentInvoice in invoiceCollection)
   {
      if (sendToCollection.IsSatisfiedBy(currentInvoice))
      {
         currentInvoice.SendToCollection(collectionAgencyService.Object);
         Console.WriteLine("Invoice number {0} has been sent to the "
                           + "collection agency.",
                           currentInvoice.InvoiceNumber);
      }
   }
   
   // Assert
   Assert.AreEqual(2, InvoiceStubsAndFakes.TempInvoiceRepository.Count);
   Assert.IsNotNull(InvoiceStubsAndFakes.TempInvoiceRepository
                       .Find(number => number.InvoiceNumber == "98765"));
   Assert.IsNotNull(InvoiceStubsAndFakes.TempInvoiceRepository
                       .Find(number => number.InvoiceNumber == "65432"));
}

private static void SetupGetInvoiceMock(
   Mock<ICollectionAgencyService> collectionAgencyService)
{
   collectionAgencyService.Setup(agencyService =>
                                 agencyService
                                    .GetInvoice(It.IsAny<String>()))
      .Returns((string invoiceNumber) =>
               InvoiceStubsAndFakes.TempInvoiceRepository
                  .Find(invoice =>
                        invoice.InvoiceNumber ==
                        invoiceNumber));
}

private static void SetupSendToCollectionMock(
   Mock<ICollectionAgencyService> collectionAgencyService)
{
   collectionAgencyService
      .Setup(agencyService => agencyService
                                 .SendInvoice(It.IsAny<Invoice>()))
      .Returns((Invoice invoice) =>
                  {
                     InvoiceStubsAndFakes
                        .AddToTempInvoiceRepository(invoice);
                     return InvoiceStubsAndFakes.TempInvoiceRepository
                               .Find(tempInvoice =>
                                     tempInvoice.InvoiceNumber ==
                                     invoice.InvoiceNumber)
                            != null;
                  });
}

private static Mock<IRepository> MockRepositoryGetAllInvoices()
{
   var repository = new Mock<IRepository>();
   repository.Setup(repo => repo.GetAllInvoices())
      .Returns(() => InvoiceStubsAndFakes.AllInvoices);
   return repository;
}

Autsch! Das ist eine Menge Code. Um das ganze etwas zu entzerren, habe ich die Mock-Setups in einzelne Methoden verpackt.
Der Test wird grün. Das heißt, die Rechnung “98765” wurde wie erwartet dem Inkassobüro übermittelt.
Hier werden weitere Architekturfehler korrigiert. Service.GetInvoices war bislang fest mit einer externen Komponente verdrahtet und ebenso Invoice.SendToCollection. Die neue Architektur sieht jetzt so aus:

/// <summary>
/// Connects to the business database.
/// </summary>
internal static class Service
{
   /* Note: Schlechte Architektur. Mit deser Signatur müsste diese Methode
    * die Serviceverbindung selbsttätig und fest gekoppelt vornehmen. */
   [Obsolete("Architekturfehler", true)]
   public static IEnumerable<Invoice> GetInvoices()
   {
      throw new NotImplementedException();
   }

   public static IEnumerable<Invoice> GetInvoices(IRepository repository)
   {
      return repository.GetAllInvoices();
   }
}

//======================================================================

public interface IRepository
{
   IEnumerable<Invoice> GetAllInvoices();
   // Add more...
}

//======================================================================

public class Repository : IRepository
{
   public IEnumerable<Invoice> GetAllInvoices()
   {
      throw new NotImplementedException();
   }
}

//======================================================================

public class Invoice
{
   public string InvoiceNumber { get; set; }

   public Customer @Customer { get; set; }

   public DateTime DueDate { get; set; }

   public bool AtCollectionAgency { get; set; }

   /* Note: Schlechte Architektur. Mit deser Signatur müsste diese Methode
    * die Serviceverbindung selbsttätig und fest gekoppelt vornehmen. */
   [Obsolete("Architekturfehler", true)]
   public void SendToCollection()
   {
      throw new NotImplementedException();
   }

   /// <summary>
   /// Sendet die Rechnung an den CollectionAgencyService.
   /// </summary>
   /// <param name="service">Eine konkrete Service-Instanz, die den
   /// ICollectionAgencyService-Contract erfüllt.</param>
   /// <returns><c>true</c> wenn die Rechnung erfolgreich übermittelt wurde,
   /// anderenfalls <c>false</c>.</returns>
   public bool SendToCollection(ICollectionAgencyService service)
   {
      bool successfullySent = service.SendInvoice(this);
      if(successfullySent)
         AtCollectionAgency = true;
      return successfullySent;
   }
}

GetInvoices bekommt jetzt eine konkrete Implementierung des neuen IRepository-Interfaces. Ich habe dieses mal Method-Injection vorgezogen weil ich die Service-Klasse sonst nichtstatisch hätte machen müssen.
Auf die gleiche Weise bin ich mit der SendToCollection-Methode verfahren.

Übrigens, das komplette Projekt können sie hier downloaden.

von Rainer Hilmer, 06.08.2010 zugeordnet zu IT Professional , Tutorials .

Kommentare

Es sind noch keine Kommentare vorhanden.

Eigener Kommentar

Sie müssen angemeldet sein, um ein Kommentar zu erstellen.
  • Schwierigkeit: Profis
  • Views: 2093
  • Zur Druckversion
  • Artikel von Rainer Hilmer

Kick it on dotnet-kicks.de

Artikel

Autor

Kick it!

Wenn ihnen dieser Artikel gefällt, bitte "kicken" sie ihn.

WPF Forum | ASP.NET Forum | ASP.NET MVC Forum | Silverlight Forum | Windows Phone 7 Forum | SharePoint Forum | Dotnet Jobs | Dotnet Termine | Developer Blogs | Dotnet News

Das Team | Regeln | Impressum