C# - testning del 2
Nu ska vi fördjupa oss i enhetstestning i NUnit och träna på att skriva enhetstester. I C# - testning gick vi igenom enhetstestning, hur vi skapade ett testprojekt med några enhetstester samt körde testerna. Läs igenom den artikeln som repetition.
Anledningen till att vi enhetstestar är att verifiera att vårt program gör det som kraven specificerar. klassen med dess attribut och metoder gör det den ska. Enhetstester säkerställer att varje del av koden fungerar som den ska. Dessutom har flera studier visat att ju tidigare vi hittar buggarna desto billigare blir utvecklingskostnaden. Enhetstesterna kan ses som en slags dokumentation på vad klassen gör.
Nackdelen är att utvecklingen tar lite längre tid och att det kräver kunskap om C#, kraven och NUnit för att skriva testfall.
NUnit är ett open-source testramverk för enhetstestning och användbart för C# och andra .NET-språk. Med NUnit skapar vi ett testprojekt med testklasser. Vi har en eller flera testklasser för en klass. NUnit erbjuder ett testbibliotek med konstruktioner för att skapa tester och för att verifiera med assertmetoder.
NUnit projektet är en del av .NET Foundation som ger vägledning och stöd för att säkerställa NUnits projektets framtid. Aktuell version är NUnit 3 som skapades av Charlie Poole, Rob Prouse, Simone Busoli och Neil Colvin samt flera andra har bidragit till förbättringar.
I vårt fall så har vi valt NUnit för vår enhetstestning och våra tester ska vara automatiska. Vi testar ett program med 4 klasser; Bank, BankAccount, InterestAccount och PayrollAccount.
Information om testfall
Section titled “Information om testfall”Vi skriver testfall genom att:
- skapa ett tillstånd (Arrange)
- utföra en eller flera handlingar (Act)
- verifiera utfallet (Assert).
Attribut
Section titled “Attribut”Olika attribut identifierar testklasser och testmetoder. Här är några exempel:
- TestFixture, identifierar att klassen är en testklass
- Test, identifierar att det är en testmetod
- TestCase, identifierar att det är en testmetod med inparametrar
- Setup, identifierar en metod som körs före varje testfall
- Teardown, identifierar en metod som körs efter varje testfall
- OneTimeSetup, identifierar en metod som körs en gång först i testklassen
- OneTimeTearDown, identifierar en metod som körs en gång sist i testklassen
Här är beskrivningar av alla attribut.
Assertions
Section titled “Assertions”Vi kommer att använda den nyare modellen, “Constraint model” då den har tydligare felmeddelanden och fortfarande är under utveckling. I NUnit är använder vi “constraints” för att verifiera ett utfall (assertion). Några exempel på användbara “constraints”:
- Equal, jämför om värden är lika. Exemplen nedan visar fler “constraints” Not och IgnoreCase.
Assert.That(2 + 2, Is.EqualTo(4.0));Assert.That(2 + 2, Is.Not.EqualTo(5));Assert.That("Hello!", Is.Not.EqualTo("HELLO!")); // Not constraintAssert.That("Hello!", Is.EqualTo("HELLO!").IgnoreCase); // IgnoreCase constrain- True, verifierar om något är sant och det finns även False som verifierar om något är falskt.
flag = true;Assert.That(flag, Is.True); // testfallet går rättAssert.That(flag, Is.False); // testfallet går fel- SubString, verifierar om utfallet innehåller en substring.
string phrase = "Make your tests fail before passing!";
Assert.That(phrase, Does.Contain("tests fail"));Assert.That(phrase, Does.Not.Contain("tests pass"));Assert.That(phrase, Does.StartWith("Make")); // StartWith constraintAssert.That(phrase, Does.EndWith("!"));- Or, verifierar om antingen det ena eller det andra utfallet är korrekt och i så fall blir testet korrekt.
Assert.That(3, Is.LessThan(10).Or.GreaterThan(5));- Count, verifierar att listan har ett visst antal element.
Assert.That(list, Has.Count.EqualTo(3)); // om listan innehåller exakt 3 elementAssert.That(list, Does.Contain(5)); // om listan innehåller värdet 5Här finns också GreaterThan, LessThan, False och liknande constraints. Läs mer om olika Constraints.
Indata till testfall
Section titled “Indata till testfall”NUnit erbjuder också TestCaseAttribute för att tillhandahålla inline-data till testmetoder. Här är ett exempel på hur man använder det:
[TestCase(12, 3, 4)][TestCase(12, 2, 6)][TestCase(12, 4, 3)]public void DivideTest(int n, int d, int q){ Assert.That(n / d, Is.EqualTo(q));}Första gången testfallet körs så divideras 12 med 3 och svaret ska bli 4. Andra gången testfallet körs så divideras 12 med 2 och svaret ska då bli 6.
Kör testfall
Section titled “Kör testfall”Men vill du köra testerna enklare i VS Code så kan du installera en extension som heter .NET Core Test Explorer.
Skapa testprojekt
Section titled “Skapa testprojekt”Vi börjar med projektet vi ska testa. Först skapar vi en katalog som heter Testing i practice-katalogen med mkdir Testing. Därefter kopierar vi vår egen kod från MyBank med cp -r MyBank Testing/MyBank2.
För att köra tester skapar vi ett testprojekt MyBank.Tests som ska testa vårt bankprojekt MyBank2. MyBank låter vi vara för det tillhör artikeln om arv. När vi börjar denna artikel så har MyBank2 samma innehåll som MyBank men olika namespaces.
Det skapas en testklass när vi skapar vårt testprojekt och den byter vi namn på till BankAccountTests.cs. Vi lägger också till en referens till vårt bankprojekt så att testklasserna hittar koden som ska testas i MyBank2.
dotnet new nunit -o MyBank2.Testscd MyBank2.Testsmv UnitTest1.cs BankAccountTests.csdotnet add reference ../MyBank2/MyBank2.csprojKatalogstrukturen i Testing ser ut så här:
Section titled “Katalogstrukturen i Testing ser ut så här:”
Katalogstrukturen i Testing ser ut så här:
Section titled “Katalogstrukturen i Testing ser ut så här:”// stå i kmom05/Testing/MyBank2.Teststree .. -L 2 // för att titta på MyBank2 och MyBank2.Tests.├── MyBank2│ ├── MyBank2.csproj│ ├── Program.cs│ ├── src│ ├── bin│ └── obj└── MyBank2.Tests ├── BankAccountTests.cs ├── MyBank2.Tests.csproj ├── bin └── objDubbelkolla att referensen till MyBank2 finns i projektfilen MyBank2.Tests.csproj till MyBank2.Tests. Dessutom kan filen MyBank2.Tests.sln också finnas och den används i Visual Studio men inte i VS Code, som vi använder. Du kan ta bort MyBank2.Tests.sln.
Kör alla testfallen
Section titled “Kör alla testfallen”dotnet testdotnet test --logger "console;verbosity=normal" // kör testerna med snygg utskriftKör enstaka testfall
Section titled “Kör enstaka testfall”dotnet test --filter Create // kör testfall med Create i testnamnetdotnet test --filter Name~TestCreateBankAccount // kör testfallet med namnet TestCreateBankAccountdotnet test --filter Priority=2 // kör tester som kommenteras med [Priority(2)].Skapa testfall till MyBank2
Section titled “Skapa testfall till MyBank2”Testfall, TestCreateBankAccount
Section titled “Testfall, TestCreateBankAccount”Vi läser i testplanen Testplan - Bankexemplet för klassen BankAccount och skapar det första testfallet som ska verifiera att en instans av klassen BankAccount skapas och att antalet konto i banken är noll.
Koden för testklassen BankAccountTests med testfallet TestCreateBankAccount.
Section titled “Koden för testklassen BankAccountTests med testfallet TestCreateBankAccount.”
Koden för testklassen BankAccountTests med testfallet TestCreateBankAccount.
Section titled “Koden för testklassen BankAccountTests med testfallet TestCreateBankAccount.”// i kmom05/MyBank2.Tests/BankAccountTests.csusing MyBank2.src;
[TestFixture]public class BankAccountTests{ [Test] public void TestCreateBankAccount() { // Arrange var account = new BankAccount("Test person", 1000); // Assert and no Act Assert.That(account.GetOwner(), Does.Contain("Test person")); Assert.That(account.GetBalance(), Is.EqualTo(1000)); }}Testkör och se att testfallet gick bra. Testkör de olika kommandona ovan för att verifiera detta första testfall. Det är bra att kunna köra ett specifikt testfall när det utvecklas.
Testfall, TestCreateBankAccountNegative
Section titled “Testfall, TestCreateBankAccountNegative”Nu går vi vidare till nästa testfall enligt testplanen och det är TestCreateBankAccountNegative.
Koden för testfallet TestCreateBankAccountNegative.
Section titled “Koden för testfallet TestCreateBankAccountNegative.”
Koden för testfallet TestCreateBankAccountNegative.
Section titled “Koden för testfallet TestCreateBankAccountNegative.”// i kmom05/Testing/MyBank2.Tests/BankAccountTests.csusing MyBank2.src;
[TestFixture]public class BankAccountTests{ [Test] public void TestCreateBankAccountNegative() { // Arrange & Assert Assert.That(() => new BankAccount("Test person", -1000), Throws.TypeOf<CreateBankAccountException>() .With.Message.EqualTo("Bankkontot ska skapas med saldo >= 0.")); }}Vi testkör och upptäcker att vi får problem. Testerna kompilerar inte eftersom vi inte har ett Exception som heter “CreateBankAccountException”. Då lägger vi till ett sådant exception med rätt meddelande.
// i kmom05/Testing/MyBank2/src/CreateBankAccountException.csnamespace MyBank2.src;
public class CreateBankAccountException : Exception{ public CreateBankAccountException(string message) : base(message) { }}Eftersom vi ändrat i projektet MyBank2 så måste vi kompilera om det innan vi testkör testprojektet på nytt.
När vi kör testprojektet på nytt så kompilerar det, men testfallet går inte igenom. Vi undersöker varför. I testplanen står det att ett exception ska kastas när ett objekt av BankAccount skapas med negativt saldo. Objekt skapas med konstruktorn och där saknas kontrollen av saldot.
Då behöver vi uppdaterar konstruktorn i klassen BankAccount så att den kastar detta fel med felmeddelandet om någon försöker skapa ett BankAccount-objekt med negativt saldo.
Koden för konstruktorn till klassen BankAccount.
Section titled “Koden för konstruktorn till klassen BankAccount.”
Koden för konstruktorn till klassen BankAccount.
Section titled “Koden för konstruktorn till klassen BankAccount.”// i kmom05/Testing/MyBank2/src/BankAccount.csnamespace MyBank2.src;
public class BankAccount{ private static int _accountNumberSeed = 1234567890; protected string _owner; protected decimal _balance; private string _accountNumber;
public BankAccount(string name, decimal initialBalance) { if (initialBalance < 0) { throw new CreateBankAccountException("Bankkontot ska skapas med saldo >= 0."); } _owner = name; _balance = initialBalance; _accountNumber = _accountNumberSeed.ToString(); _accountNumberSeed++; } ...Kompilera om projektet MyBank2 och kör testprojektet på nytt. Nu går testfallet igenom.
Testfall, TestCreateBank
Section titled “Testfall, TestCreateBank”Vi läser i testplanen Gör en testplan för Bankexemplet för klassen Bank och skapar det första testfallet som ska verifiera att en instans av klassen Bank skapas och att antalet konto i banken är noll.
Vi har ingen klass Bank i projektektet MyBank2 men vi skapar den så här.
Koden för bank klassen:
Section titled “Koden för bank klassen:”
Koden för bank klassen:
Section titled “Koden för bank klassen:”// i kmom05/Testing/MyBank2/src/Bank.csnamespace MyBank2.src;
public class Bank{public class Bank{ public const string BANK_NAME = "Dbwebb Bank"; private List<BankAccount> _accounts;
public Bank(string bankName) { this._accounts = new List<BankAccount>(); }
public void AddAccount(BankAccount newAccount) { this._accounts.Add(newAccount); }
public string GetNoOfAccounts() { return this._accounts.Count.ToString(); }
public string GetDescription() { return $"Banken heter {BANK_NAME} och har {GetNoOfAccounts()} konton."; }}Här visas hela testklassen BankTests med testfallet TestCreateBank. I Setupmetoden skapas en instans av klassen Bank med namnet från konstanten BANK_NAME. I testfallet TestCreateBank verifierar vi att bankens namn blev det vi satte samt att antalet konto i en tom bank är 0.
Koden för testklassen BankTest med testfallet TestCreateBank:
Section titled “Koden för testklassen BankTest med testfallet TestCreateBank:”
Koden för testklassen BankTest med testfallet TestCreateBank:
Section titled “Koden för testklassen BankTest med testfallet TestCreateBank:”// i kmom05/Testing/MyBank2.Tests/BankTests.csusing MyBanks2.src;
[TestFixture]public class BankTests{ private Bank _bank; private const string BANK_NAME = "Test bank";
[SetUp] public void Setup() { // Arrange _bank = new Bank(); }
[Test] public void TestCreateBank() { // Assert and Act Assert.That(Bank.BANK_NAME, Does.Contain(BANK_NAME)); Assert.That(_bank.GetNoOfAccounts(), Does.Contain("0")); }}Vi testkör och nu går de tre testfallen igenom. Bra!
Testfall, TestCreateBankDescription
Section titled “Testfall, TestCreateBankDescription”Vi läser vidare i testplanen och ser att nästa testfall är TestCreateBankDescription. Det är bra att testa beskrivningen i ett eget testfall eftersom den kan komma att ändras.
Koden för testfallet TestCreateBankDescription:
Section titled “Koden för testfallet TestCreateBankDescription:”
Koden för testfallet TestCreateBankDescription:
Section titled “Koden för testfallet TestCreateBankDescription:”// i kmom05/Testing/MyBank2.Tests/BankTests.csusing MyBanks2.src;
[TestFixture]public class BankTests{ ...
[Test] public void TestCreateBankDescription() { // Assert and Act Assert.That(_bank.GetDescription(), Does.Contain($"Banken heter {Bank.BANK_NAME} och har {_bank.GetNoOfAccounts()} konton.")); }}Vi testkör och ser att testfallet ger grönt, det vill säga det går igenom.
Mockning
Section titled “Mockning”För att mocka testfall i NUnit kan du använda NUnit’s inbyggda mock-bibliotek eller andra populära bibliotek som Moq eller Rhino Mocks.
Mockning används i mjukvarutestning för att simulera beteendet hos externa beroenden, som andra klasser, databaser eller filer. Istället för att kommunicera med en riktig databas så mockar vi databasen och kommunicerar med mockobjektet av databasen.
Skillnad mockning och stubbning
Section titled “Skillnad mockning och stubbning”Fake är en generisk term som kan användas för att beskriva antingen en stubb eller ett mockobjekt.
Ett mockobjekt, mock, är ett objekt som imiterar beteendet hos riktiga objekt. Mockobjekt används för att verifiera interaktionerna mellan klasser eller beteendet hos klasser. Före starten av ett test ställer vi in förväntningarna på en mock och instruerar den att bete sig på ett visst sätt när något händer. Det innebär att vi kan kontrollera om vissa metoder har anropats, hur många gånger de har anropats och med vilka parametrar. Ett mockobjekt imiterar ett verkligt objekt.
Stubbing ger fördefinierade svar på anrop som görs under testet. Vi använder stubbar när vi vill ersätta ett beroende med en enkel implementation som returnerar fördefinierade värden. Det är mindre fokuserat på interaktion och mer på att tillhandahålla nödvändiga data för testet.
Steg för att mocka
Section titled “Steg för att mocka”Installera Moq
Section titled “Installera Moq”Vi börjar med att installera “Moq” för att kunna använda det.
// stå i testprojektet MyBank2.Testsdotnet add package MoqVerifiera att den är installerad genom att kolla i projektfilen.
Skapa en mock i ett testfall
Section titled “Skapa en mock i ett testfall”Vi kan skapa en mock av ett objekt med hjälp av mock-biblioteket. Till exempel, om vi använder Moq kan vi skapa en mock med var mock = new Mock<YourClass>();.
Vi antar att “skapa konto” med klassen BankAccount har yttre beroenden och därför behöver vi mocka “skapa konto”.
Vi gör ett exempel och skapar testfallet TestAddAccounts. Testfallen finns beskrivna i artikeln Gör en testplan för Bankexemplet. Vi lägger till att vi använder Moq genom using Moq;.
Koden för testfallet TestAddAccounts:
Section titled “Koden för testfallet TestAddAccounts:”
Koden för testfallet TestAddAccounts:
Section titled “Koden för testfallet TestAddAccounts:”// i kmom05/Testing/MyBank2.Tests/BankTests.csusing Moq;using MyBank.src;
[TestFixture]public class BankTests{ ...
[Test] public void TestAddAccounts() { // Arrange var mockAccount1 = new Mock<BankAccount>("Owner 1", 12000m); var mockAccount2 = new Mock<BankAccount>("Owner 2", 2000m);
// Act _bank.AddAccount(mockAccount1.Object); _bank.AddAccount(mockAccount2.Object);
// Assert Assert.That(_bank.GetNoOfAccounts(), Does.Contain("2")); }}I testfallet TestAddAccounts skapar vi ett mockobjekt för klassen BankAccount istället för ett objekt av själva klassen. var används här för att förenkla koden och göra den mer lättläst. Kompilatorn kan härleda att mockBank är av typen Mock<BankAccount> eftersom den tilldelas ett nytt objekt av denna typ.
Inom <> efter new Mock så visar vi typen på mockobjektet, vilket i exemplet är BankAccount. För att enkelt hålla koll på att det är ett mockobjekt får det namnet “mockAccount” (med mock i början av namnet). Vi har bland annat skapat ett mockobjekt av typen BankAccount som heter “mockAccount1” och för att komma åt vårt BankAccount-objekt använder vi mockAccount1.Object.
Koden för testfallet TestNoOfAccounts:
Section titled “Koden för testfallet TestNoOfAccounts:”
Koden för testfallet TestNoOfAccounts:
Section titled “Koden för testfallet TestNoOfAccounts:”// i kmom05/Testing/MyBank2.Tests/BankTests.cs...
[Test] public void TestAddAccounts() { // Arrange var mockAccount1 = new Mock<BankAccount>("Owner 1", 12000m); var mockAccount2 = new Mock<BankAccount>("Owner 2", 2000m);
// Act _bank.AddAccount(mockAccount1.Object); _bank.AddAccount(mockAccount2.Object);
// Assert Assert.That(_bank.GetNoOfAccounts(), Does.Contain("2")); }}I testfallet TestAddAccounts skapar vi ett mockobjekt för klassen BankAccount istället för ett objekt av själva klassen. var används här för att förenkla koden och göra den mer lättläst. Kompilatorn kan härleda att mockBank är av typen Mock<BankAccount> eftersom den tilldelas ett nytt objekt av denna typ.
Inom <> efter new Mock så visar vi typen på mitt mockobjekt, vilket i exemplet är BankAccount. För att enkel hålla koll på att det är ett mockobjekt får det namnet “mockAccount” (med mock i början av namnet). Vi har bland annat skapat ett mockobjekt av typen BankAccount som heter “mockAccount1” och för att komma åt vårt BankAccount-objekt så använder vi mockAccount1.Object.
Sammanfattning
Section titled “Sammanfattning”Nu har vi tittat på mer enhetstestning C#:
- NUnit med assertions enligt den nyare “Constraint model”
- att skriva testfall
- att köra testfall (dotnet test), alla, en klass eller ett testfall
- vi har provat på att använda .NET Core Test Explorer för att kunna köra testfall i VS code.
- mocka klasser i testfall
Koden för Testing/MyBank2 laddar du ned det med task download-code -- kmom05/Testing/MyBank2 och koden för Testing/MyBank2.Tests laddar du ned det med task download-code -- kmom05/Testing/MyBank2.Tests. De båda projekten hamnar då i katalogen kmom05/Testing/codeExamples/.
Vill du läsa mer om enhetstester på Microsofts sätt, kolla här.