Fluent Assertions gör dina tester enklare att förstå


Idag är det en självklarhet att skriva automatiserade tester vid utveckling. Många tillämpar även TDD (Test Driven Development). Testerna ger en säkerhet när man arbetar med koden eftersom de talar om när något gått sönder. Välskrivna tester kan även fungera som dokumentation. Nya utvecklare kan skapa sig en bild av vad systemet gör genom att kolla på testerna. Genom att använda Fluent Assertions så blir valideringen av testerna mycket lättläst och enkel att följa i jämförelse med de mer traditionella metoderna som erbjuds i många testramverk.

Uppbyggnad av ett test

Normalt delar man in ett test i tre olika delar:

  • Arrange – skapar de förutsättningar som testet kräver i form av resurser och tjänster
  • Act – utför det som ska testas
  • Assert – verifierar utfallet av testet

Så vad gör Fluent Assertions?

Fluent Assertions är ett .NET-bibliotek som riktar in sig på den sista delen som gäller ”Assert”. Vanligtvis använder man testramverkets Assert-metoder för att verifiera resultatet av testen. En nackdel med dessa metoder är att testerna blir väldigt tekniska till sin natur och det kan vara svårt att se exakt vad man testar. Med Fluent Assertions så blir koden för att verifiera resultaten mycket enklare att förstå. Fluent Assertions är egentligen ett bibliotek med ”extension methods”. Det gör att man kan kedja ihop flera olika anrop och på så sätt få ett mycket mer uttrycksfullt sätt att skriva valideringskoden.

Test av en kalkylator

Som exempel så testar vi följande klass som är en mycket enkel kalkylator.


using System;
using System.Collections.Generic;

namespace CalculatorLib
{
  public class Calculator
  {
    private Stack _history = new Stack();

    public int Memory
    {
      get;
      set;
    }

    public IEnumerable History
    {
      get { return _history; }
    }

    public Calculator()
    {
    }

    public int Add(int num1, int num2)
    {
      var result = num1 + num2;
      _history.Push(result);
      return result;
    }

    public int Subtract(int num1, int num2)
    {
      var result = num1 - num2;
      _history.Push(result);
      return result;
    }

    public int Multiply(int num1, int num2)
    {
      var result = num1 * num2;
      _history.Push(result);
      return result;
    }

    public double Divide(int num1, int num2)
    {
      if (num2 == 0)
        throw new ArgumentException("Zero is not allowed", nameof(num2));

      return Convert.ToDouble(num1) / Convert.ToDouble(num2);
    }
  }
}

Tester skrivna med nUnits assert-metoder

Såhär skulle tester kunna se ut om de skrivs på vanligt sätt med nUnits assert-metoder.


using CalculatorLib;
using NUnit.Framework;
using System;
using System.Collections.Generic;

namespace CalculatorLibraryTest
{
  [TestFixture]
  public class ClassicCalculatorLibTests
  {
    [Test]
    public void TestAddition()
    {
      // Arrange
      var calculator = new Calculator();

      // Act 
      var result = calculator.Add(2, 4);

      // Assert
      Assert.AreEqual(6, result, "incorrect sum of 2 + 4");
    }

    [Test]
    public void TestSubtraction()
    {
      // Arrange
      var calculator = new Calculator();

      // Act 
      var result = calculator.Subtract(10, 3);

      // Assert
      Assert.AreEqual(7, result, "incorrect difference of 10 - 3");
    }

    [Test]
    public void TestMultiplication()
    {
      // Arrange
      var calculator = new Calculator();

      // Act 
      var result = calculator.Multiply(5, 3);

      // Assert
      Assert.AreEqual(15, result, "incorrect product of 5 * 3");
    }

    [Test]
    public void TestDivision()
    {
      // Arrange
      var calculator = new Calculator();

      // Act
      var result = calculator.Divide(15, 3);

      // Assert
      Assert.AreEqual(5d, result, "incorrect quote of 15 / 3");
    }

    [Test]
    public void TestDivisionByZero()
    {
      // Arrange
      var calculator = new Calculator();

      // Act
      Action act = () => calculator.Divide(15, 0);

      // Assert
      ArgumentException ex = Assert.Throws(new TestDelegate(act));
      Assert.That(ex.Message, Is.StringStarting("Zero is not allowed"));
      Assert.That(ex.ParamName, Is.EqualTo("num2"));
    }

    [Test]
    public void TestMemory()
    {
      // Arrange
      var calculator = new Calculator();

      // Act
      var sum = calculator.Add(2, 9);
      calculator.Memory = sum;
      var result = calculator.Memory;

      // Assert
      Assert.AreEqual(11, result, "incorrect value in memory");
    }

    [Test]
    public void TestResultHistory()
    {
      // Arrange
      var calculator = new Calculator();
      var sum = calculator.Add(1, 3);
      var difference = calculator.Subtract(5, 2);
      var product = calculator.Multiply(6, 3);
      var expected = new Stack();
      expected.Push(sum);
      expected.Push(difference);
      expected.Push(product);

      // Act
      var history = calculator.History;

      // Assert
      CollectionAssert.IsNotEmpty(history);
      CollectionAssert.AreEqual(expected, history);
    }
  }
}

I detta fall använder vi metoden Assert samt CollectionsAssert för att validera utfallet av testet. Även om det går att förstå vad koden gör så kräver det en del tankeverksamhet för att förstå vad som testas och framförallt vad det förväntade resultatet ska vara.

Tester skrivna med Fluent Assertions

Så här kan motsvarande tester skrivas med Fluent Assertions.


using NUnit.Framework;
using FluentAssertions;
using CalculatorLib;

using System;
namespace CalculatorLibTest
{
  [TestFixture()]
  public class CalculatorTest
  {
    [Test()]
    public void TestAddition()
    {
      // Arrange
      var calculator = new Calculator();

      // Act
      var result = calculator.Add(2, 4);

      // Assert
      result.Should().Be(6,"because it's the sum of 2 and 4");
    }

    [Test]
    public void TestSubtraction()
    {
      // Arrange
      var calculator = new Calculator();

      // Act
      var result = calculator.Subtract(10, 3);

      // Assert
      result.Should().Be(7, "because it's the difference between 10 and 3");
    }

    [Test]
    public void TestMultiplication()
    {
      // Arrange
      var calculator = new Calculator();

      // Act
      var result = calculator.Multiply(5, 3);

      // Assert
      result.Should().Be(15, "because it's the product of 5 and 3");
    }	

    [Test]
    public void TestDivision()
    {
      // Arrange
      var calculator = new Calculator();

      // Act
      var result = calculator.Divide(15, 3);

      // Assert
      result.Should().Be(5.0d, "because it's the quote of 15 and 3");
    }

    [Test]
    public void TestDivisionByZero()
    {
      // Arrange
      var calculator = new Calculator();

      // Act
      Action act = () => calculator.Divide(15, 0);

      // Assert
      act.ShouldThrow("because division by zero is not allowed")
        .Where(e => e.Message.StartsWith("Zero is not allowed"))
        .And.ParamName.Should().BeEquivalentTo("num2");
    }

    [Test]
    public void TestMemory()
    {
      // Arrange
      var calculator = new Calculator();

      // Act
      var sum = calculator.Add(2, 9);
      calculator.Memory = sum;
      var result = calculator.Memory;

      // Assert
      result.Should().Be(11, "because the sum of 2 and 9 should be stored in memory");
    }

    [Test]
    public void TestResultHistory()
    {
      // Arrange
      var calculator = new Calculator();
      var sum = calculator.Add(1, 3);
      var difference = calculator.Subtract(5, 2);
      var product = calculator.Multiply(6, 3);

      // Act
      var history = calculator.History;

      // Assert
      history.Should().NotBeEmpty()
        .And.HaveCount(3).And
        .ContainInOrder(product,difference,sum);
    }
  }
}

Fluent Assertions låter oss tala om vad utfallet borde vara (därav metoden Should()). Vi kan sedan länka på ytterligare villkor som vi förväntar oss ska vara uppfyllda. Det är ganska enkelt att förstå vad det förväntade utfallet ska vara genom att bara läsa koden. Fluent Assertions gör att koden blir lätt att förstå även för någon som inte kan programmering.

Vad kan Fluent Assertions göra mer?

Fluent Assertions kan användas för att validera struktur i projekt t.ex. att projekt refererar varandra på rätt sätt t.ex. att presentationslager endast refererar affärslogikslagret och att det i sin tur refererar datalagret. På så sätt kan man bygga in tester för arkitektur. Det finns även tillägg till Fluent Assertions som kan testa ”dependency injection” som t.ex. ninject.

Läs mer på Fluent Assertions