Ordnung in den BDD Specs mit xUnit.BddExtensions und BehaviorConfigs

Written on November 21, 2010

Triple-A ftw!

Bei der testgetriebenen Entwicklung (TDD) hat sich inzwischen die AAA-Syntax durchgesetzt.

AAA steht für

  1. Arange
  2. Act
  3. Assert

Im Arrange-Part werden die zu testenden Objekte (Sut = System under test) sowie deren Abhängigkeiten konfiguriert.

Im Act-Part wird das zu testende Verhalten aufgerufen und das Resultat daraus wird im Assert gegen entsprechende Bedingungen getestet.

Don't repeat yourself

Gerade der Arange-Part kann schnell sehr umfangreich werden, besonders bei Brownfield- oder COM-Projekten, wo viele Mock-Objekte benötigt werden.

Häufig ist es aufgrund der AAA-Struktur auch so, dass man bestimmte Mock-Objekte in mehreren Tests immer wieder (gleich) konfiguriert.

Nun kann man natürlich bestimmte Konfigurationen von Mock-Objekten in private Methoden innerhalb der Test-Klasse auslagern und entsprechend verwenden oder ähnliche Hilfskonstrukte erstellen.

Man kann aber auch gleich die xUnit.BddExtensions verwenden und sich der dort enthaltenen BehaviorConfigs bedienen.

Diese erlauben es, außerhalb von EstablishContext zu konfigurieren und dann innerhalb von EstablishContext aufzurufen und in Because bzw. den Specs zu verwenden.

Um das ganze zu demonstrieren, verwenden wir die Lichtschalter-Specs aus dem letzten Posting.

Allerdings soll das Event "LightSwitched" des ILight-Interface nicht mehr in der Because-Methode (=Act) feuern, sondern die Methode SwitchLight(LightState lightState) verwenden, welche dann das Event feuern soll.

Unsere Spec sähe also so aus -- EstablishContext fehlt im Moment absichtlich:

public class When_light_is_switched_on : InstanceContextSpecification<ILightSensor> {
    protected override ILightSensor CreateSut() {
        return new LightSensor();
    }

    protected override void Because() {
        Sut.Watch(The<ILight>());
        The<ILight>().SwitchLight(LightState.On);
    }

    [Observation]
    public void Should_be_detected_by_sensor() {
        Sut.LightState.ShouldBeEqualTo(LightState.On);
    }
}

Im Normalfall würden wir nun in EstablishContext folgendes Konstrukt einfügen:

protected override void EstablishContext() {
    The<ILight>().WhenToldTo
        (l => l.SwitchLight(LightState.On)).Return(0).WhenCalled(
            invocation =>
    The<ILight>()
        .Event(l => l.LightSwitched += (sender, args) => { })
        .Raise(The<ILight>(), new LightSwitchedHandlerArgs(LightState.On)));
}

Eben dieses können wir aber mit den xUnit.BddExtensions auch in eine BehaviorConfig auslagern:

public class LightSwitchedOff : IBehaviorConfig {
    public void EstablishContext(IDependencyAccessor accessor) {
        accessor.The<ILight>().WhenToldTo
            (l => l.SwitchLight(LightState.On)).Return(0).WhenCalled(
                invocation =>
        accessor.The<ILight>()
            .Event(l => l.LightSwitched += (sender, args) => { })
            .Raise(accessor.The<ILight>(), new LightSwitchedHandlerArgs(LightState.On)));
    }

    public void PrepareSut(object sut) {
    }

    public void Cleanup(object sut) {
    }
}

Die Verwendung der BehaviorConfig erfolgt dann in EstablishContext:

[Concern(typeof (ILightSensor))]
public class When_light_is_switched_on : InstanceContextSpecification<ILightSensor> {
    protected override void EstablishContext() {
        With<LightSwitchedOff>();
    }

    protected override ILightSensor CreateSut() {
        return new LightSensor();
    }

    protected override void Because() {
        Sut.Watch(The<ILight>());
        The<ILight>().SwitchLight(LightState.On);
    }

    [Observation]
    public void Should_be_detected_by_sensor() {
        Sut.LightState.ShouldBeEqualTo(LightState.On);
    }
}

Aufgrund der Tatsache, dass man mehrere BehaviorConfigs in EstablishContext verwenden kann, erhält man hier die Möglichkeit, Mocks modular mit Funktionen auszustatten ohne ein und denselben Code wieder und wieder zu schreiben.

Die Beispielanwendung kann hier heruntergeladen werden:

BehaviorConfigsBddExtensions.zip (1.32 mb)

DotNetKicks-DE Image