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 😉