Static Gateway Pattern [endlich-clean.net]

Oft stellt man bei Unit Tests die nach der Implementierung erstellt werden fest, dass statische Klassen oder Methoden verwendet werden. Dabei stellt sich dann die Frage, wie man diese im Test isolieren kann. Ein einfaches Beispiel soll die Problematik verdeutlichen:

   1: public class Task

   2: {

   3:     private string name;

   4:     private string projectName;

   5:  

   6:     public Task CreateNewTask(string taskName) {

   7:         return new Task {

   8:             name = taskName,

   9:             projectName = ProjectManager.CurrentProjectManager.CurrentProject().Name

  10:         };

  11:     }

  12:  

  13:     public string Name {

  14:         get { return name; }

  15:     }

  16:  

  17:     public string ProjectName {

  18:         get { return projectName; }

  19:     }

  20: }

Der Knackpunkt ist in Zeile 9 zu finden. Hier wird auf einen ProjectManager zugegriffen, der offensichtlich eine statische Instanz in Form eines Singleton enthält.

Um nun die Methode CreateNewTask testen zu können, muss der Aufruf an den ProjectManager isoliert werden. Dies geht auf einfache Weise, in dem man das sogenannte static gateway Pattern anwendet. Dazu muss die Klasse, welche die statische Methode enthält, modifiziert werden. Zunächst die Variante der Klasse, die im Test nicht isoliert werden kann:

   1:  

   2: public class ProjectManager

   3: {

   4:     private static ProjectManager currentProjectManager;

   5:     private static Project currentProject;

   6:  

   7:     public static ProjectManager CurrentProjectManager {

   8:         get {

   9:             if (currentProjectManager == null) {

  10:                 currentProjectManager = new ProjectManager();

  11:             }

  12:             return currentProjectManager;

  13:         }

  14:     }

  15:  

  16:     public Project CurrentProject() {

  17:         if (currentProject == null) {

  18:             currentProject = new Project();

  19:         }

  20:         return currentProject;

  21:     }

  22: }

Hier liegt das Problem im static Property CurrentProjectManager in den Zeilen 7-14. Dort wird nämlich beim ersten Zugriff eine Instanz der Klasse ProjectManager erstellt. Dieses Singleton wird dann in allen weiteren Aufrufen zurück geliefert. Um nun Verwender dieser Klasse testen zu können, müssen wir das Verhalten der Klasse ProjectManager im Test jedoch beeinflussen können. In der oben gezeigten Implementierung kann die Klasse im Test jedoch nicht durch eine Attrappe ersetzt werden. Also schaffen wir uns diese Möglichkeit in dem wir folgende Methode ergänzen:

   1: public static void SetCurrentProjectManager(ProjectManager projectManager) {

   2:     currentProjectManager = projectManager;

   3: }

Damit können wir im Test eine Attrappe eines ProjectManagers erzeugen und in die Klasse injizieren.

Im zweiten Schritt ist es in der Regel erforderlich, ein Interface auf die Klasse zu definieren. Dies ist immer dann notwendig, wenn die Methoden, deren Verhalten im Test verändert werden soll, nicht virtuell sind, da sie dann nicht überschrieben werden können. Im konkreten Beispiel müsste ein Interface für die Klasse ProjectManager erstellt werden. Andernfalls kann das für den Test gewünschte Verhalten der Attrappe nicht implementiert werden. Mit Refaktorisierungswerkzeugen wie ReSharper lässt sich das automatisieren (Extract Interface).

Steht das Interface zur Verfügung, müssen die Verwender der Klasse auf das Interface umgestellt werden. Mit ReSharper läßt sich auch das automatisieren (Use Base Type where Possible).

Zuletzt kann dann im Test eine Attrappe anstelle des “echten” ProjectManagers verwendet werden:

   1:  

   2: [TestFixture]

   3: public class TaskTests

   4: {

   5:     private IProjectManager projectManager;

   6:  

   7:     [SetUp]

   8:     public void Setup() {

   9:         projectManager = MockRepository.GenerateMock<IProjectManager>();

  10:         ProjectManager.SetCurrentProjectManager(projectManager);

  11:         projectManager.Expect(x => x.CurrentProject()).Return(new Project("n1"));

  12:     }

  13:  

  14:     [Test]

  15:     public void Task_name_is_set_correct() {

  16:         var task = Task.CreateNewTask("t1");

  17:         Assert.That(task.Name, Is.EqualTo("t1"));

  18:     }

  19:  

  20:     [Test]

  21:     public void Task_project_name_is_set_correct() {

  22:         var task = Task.CreateNewTask("");

  23:         Assert.That(task.ProjectName, Is.EqualTo("n1"));

  24:     }

  25: }

Fazit: mit kleinen Umbauarbeiten können statische Methoden auch im Test verwendet werden. Bei einer test-first Vorgehensweise wären die Schwächen dieses Designs jedoch sofort aufgefallen, ein späterer Umbau zugunsten der Testbarkeit wäre dann nicht erforderlich.

Kick it on dotnet-kicks.de

Comments are closed.