NUnit – przykładowy test jednostkowy

Na prostym przykładzie opartym o fakturę zaprezentuję jak można się ustrzec przed błędami dzięki testom jednostkowym.

Stwórzmy klasy, które będą w podstawowym stopniu modelowały fakturę i pozycje na niej.
Pozycja na fakturze (InvoiceEntry) zawiera nazwę, kwotę netto i kwotę podatku VAT. Kwota brutto jest wyliczana na podstawie kwoty netto i stawki VAT.

  public class InvoiceEntry
  {
    public InvoiceEntry(string name, decimal netValue, decimal vatRate)
    {
      Name = name;
      NetValue = netValue;
      VatRate = vatRate;
    }

    public string Name { get; }
    public decimal NetValue { get; }
    public decimal VatRate { get; }

    public decimal GrossValue => NetValue + VatValue;

    public decimal VatValue => Math.Round(NetValue * VatRate, 2);
  }

Sama faktura (Invoice) składa się z kolekcji pozycji, danych płatnika i wystawiającego. Posiada także właściwości, które zwracają informacje o sumarycznych kwotach netto, brutto i VAT.

  public class Invoice
  {
    public Invoice()
    {
      Entries = new List<InvoiceEntry>();
    }

    public string Contractor { get; set; }
    public string Issuer { get; set; }

    public ICollection<InvoiceEntry> Entries { get; }

    public decimal SummaryNetValue => Entries.Sum(x => x.NetValue);
    public decimal SummaryVat => Entries.Sum(x => x.VatValue);
    public decimal SummaryGrossValue => Entries.Sum(x => x.GrossValue);

    public void AddEntry(InvoiceEntry entry)
    {
      Entries.Add(entry);
    }
  }

Przetestujmy teraz, czy kwota brutto dla liczb całkowitych i rzeczywistych dodatnich jest liczona prawidłowo. W przykładach wykorzystuję framework Nunit.

  [TestFixture]
  public class InvoiceEntryTests
  {
    [Test]
    public void ShouldCalculateGrossValueFor10()
    {
      var invoiceEntry = new InvoiceEntry("Zeszyt", 10, 0.23m);

      Assert.That(invoiceEntry.GrossValue, Is.EqualTo(12.30m));
    }

    [Test]
    public void ShouldCalculateVatValueForIntegerNumer()
    {
      var invoiceEntry = new InvoiceEntry("Zeszyt", 10, 0.23m);

      Assert.That(invoiceEntry.VatValue, Is.EqualTo(2.3m));
    }

    [Test]
    public void ShouldCalculateVatValueForPositiveRealNumber()
    {
      var invoiceEntry = new InvoiceEntry("Zeszyt", 9.73m, 0.23m);

      Assert.That(invoiceEntry.VatValue, Is.EqualTo(2.24m));
    }
  }

Podczas wykonywania ostatniego testu okazało się, że warunek nie jest spełniony. Otrzymujemy inną wartość niż podaliśmy do sprawdzenia. Natrafiliśmy na dziwny sposób zaokrąglania wbudowany w .NET. Jak się okazuje Math.Round(…) działa domyślnie inaczej niż to czego uczyli nas w szkołach. Dla liczb parzystych zaokrągla w górę, dla nieparzystych w dół.
Dzięki testom jednostkowym natrafiliśmy na przypadek, gdy framework działa niezgodnie z naszymi przypuszczeniami. Po przejrzeniu dokumentacji okazało się, że było to ich celowym zabiegiem. Po dodaniu flagi MidpointRounding.AwayFromZero obliczenia przebiegają już standardowo.

public decimal VatValue => Math.Round(NetValue * VatRate, 2, MidpointRounding.AwayFromZero);

Pozostało nam tylko sprawdzić, czy wszystko zachowuje się poprawnie dla całej faktury.

  [TestFixture]
  public class InvoiceTests
  {
    [Test]
    public void ShouldContainNoEntries()
    {
      var invoice = new Invoice();

      Assert.That(invoice.Entries, Is.Empty);
    }

    [Test]
    public void ShouldHaveNoValues()
    {
      var invoice = new Invoice();

      Assert.That(invoice.SummaryNetValue, Is.EqualTo(0));
      Assert.That(invoice.SummaryVat, Is.EqualTo(0));
      Assert.That(invoice.SummaryGrossValue, Is.EqualTo(0));
    }

    [Test]
    public void ShouldAddInviceEntries()
    {
      var invoice = new Invoice();

      invoice.AddEntry(new InvoiceEntry("Zeszyt", 5.5m, 0.23m));
      invoice.AddEntry(new InvoiceEntry("Długopis", 1.5m, 0.23m));

      Assert.That(invoice.Entries.Count, Is.EqualTo(2));
    }

    [Test]
    public void ShouldHaveProperNonZeroValues()
    {
      var invoice = new Invoice();

      invoice.AddEntry(new InvoiceEntry("Zeszyt", 5.5m, 0.23m)); // 6,77
      invoice.AddEntry(new InvoiceEntry("Długopis", 1.5m, 0.23m)); // 1,85

      Assert.That(invoice.SummaryNetValue, Is.EqualTo(7.0m));
      Assert.That(invoice.SummaryVat, Is.EqualTo(1.62m));
      Assert.That(invoice.SummaryGrossValue, Is.EqualTo(8.62m));
    }
  }

W związku z tym testujemy dodawanie nowych pozycji oraz wartości kwot netto, brutto i VAT dla całej faktury. Możemy zauważyć, że mamy do czynienia z kwotą, która może powodować podobne problemy jak przy pozycjach faktury. Jednak dzięki poprawce wszystko ma właściwą wartość i testy wykonują się z powodzeniem.

Jak widzieliśmy testy jednostkowe uratowały nam skórę w obliczeniach finansowych. Co prawda natrafiliśmy na błąd przypadkowo. Potwierdza to jednak, że możemy w prosty i szybki sposób wykryć rożne nieścisłości, bez siedzenia nad całym systemem i wyklikiwania poszczególnych pozycji. Nasz klient będzie zadowolony, jego klient też – nie będą oszukiwani nieświadomie na drobnych kwotach.

 

PS. Wesołych Świąt 😉

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *