sobota, 29 grudnia 2018

[1] Visual Studio - NUnit Test - Przeprowadzanie podstawowych testów jednostkowych

W tym poście chciałbym opisać podstawowy sposób przygotowania środowiska testowego oraz wykonanie podstawowych testów.


Najlepszym sposobem na wygenerowanie projektu testowego jest stworzenie nowego projektu testowego:

 

Po jego ustanowieniu za pomocą menadżera Nuget instalujemy potrzebne elementy testowe. Czyli pakiet NUnit (wraz z elementami dodatkowymi jak Console, Console Runner, Runners, NUnit3TestAdapter) oraz NSubstitudeAutoMocker (NSub zostanie przeze mnie opisany w następnym poście). Następnie należy dołożyć referencję do testowanego projektu. W przypadku problemów z działem której części aplikacji polecam ponowne uruchomienie środowiska Visual Studio.

W przypadku niemożliwości uruchomienia testów należy odznaczyć opcję "For improved performance...":


Klasa testowa przygotowana do wprowadzania testów powinna wyglądać mniej więcej tak:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;\
  4. /*
  5. using Microsoft.VisualStudio.TestTools.UnitTesting;
  6. Należy usunąć z projektu testowego, aby używać tylko jednego frameworka.
  7. */
  8. using NSubstitute;
  9. using NUnit.Framework;
  10. namespace TestowanyProjekt
  11. {
  12.     [TestFixture]
  13.     public class UnitTest1
  14.     {
  15.         [Test]
  16.         public void TestMethod1()
  17.         {
  18.         }
  19.     }
  20. }

Poniżej przedstawię kilka klas wraz z przykładowymi sposobami wykonania na nich testów.

Przykładowa klasa testowa odpowiedzialna za wykonywanie podstawowych operacji matematycznych:

  1. public interface IMathClass
  2. {
  3.     UInt32 Add(UInt32 num1, UInt32 num2);
  4.     UInt32 Subtraction(UInt32 num1, UInt32 num2);
  5. }
  6. class MathClass : IMathClass
  7. {
  8.     public UInt32 Add(UInt32 num1, UInt32 num2)
  9.     {
  10.         return num1 + num2;
  11.     }
  12.     public UInt32 Subtraction(UInt32 num1, UInt32 num2)
  13.     {
  14.             if(checkIfOperationCanBeContinue(num1, num2) == false)
  15.             {
  16.                 return 0x00;
  17.             }
  18.             return num1 - num2;
  19.     }
  20.     private bool checkIfOperationCanBeContinue(UInt32 num1, UInt32 num2)
  21.     {
  22.         if(num1 < num2)
  23.         {
  24.             return false;
  25.         }
  26.         return true;
  27.     }
  28. }

Teraz przeprowadzę testy dwóch funkcji Add oraz Substraction. W funkcji Add można przetestować tylko wynik zwracanej operacji. W funkcji Substraction można dodatkowo sprawdzić warunek zdefiniowany w środku funkcji ponieważ mamy dwie możliwe ścieżki działania funkcji po jej wywołaniu:

Test dla funkcji Add:

  1. [Test]
  2. public void MathClass_Add_AddOperationResult()
  3. {
  4.     testAppWpf.IMathClass mathClass = new testAppWpf.MathClass();
  5.     UInt32 returnValue = mathClass.Add(12);
  6.     UInt32 expectedValue = 3;
  7.     Assert.AreEqual(expectedValue, returnValue);
  8. }

Test dla działania funkcji odejmującej. Tutaj do przetestowania są dwa przypadki:

  1. [Test]
  2. public void MathClass_Subtraction_SubtractionOperationResult()
  3. {
  4.     testAppWpf.IMathClass mathClass = new testAppWpf.MathClass();
  5.     UInt32 returnValue = mathClass.Subtraction(52);
  6.     UInt32 expectedValue = 3;
  7.     Assert.AreEqual(expectedValue, returnValue);
  8. }
  9. [Test]
  10. public void MathClass_Subtracion_SubErrorWrongValues()
  11. {
  12.     testAppWpf.IMathClass mathClass = new testAppWpf.MathClass();
  13.     UInt32 returnValue = mathClass.Subtraction(25);
  14.     UInt32 expectedValue = 0x00;
  15.     Assert.AreEqual(expectedValue, returnValue);
  16. }

Testy funkcji obsługują jeden przypadek wyjątkowy, którym jest podanie złego elementu przy odejmowaniu. Nie zabezpieczam tutaj części przekroczenia zakresu dla zmiennej, spowodowane jest to dość dużym zakresem możliwych liczb do wprowadzenia.

Kolejnym elementem testowym jest klasa ustawiająca zmienną globalną po wywołaniu:

  1. public class GlobValuesMat
  2. {
  3.     public UInt32 rememberOperationData;
  4.     public UInt32 RememberOperationData
  5.     {
  6.         get { return rememberOperationData; }
  7.     }
  8.     public GlobValuesMat() { }
  9. }
  10. public class MathClass : IMathClass
  11. {
  12.     GlobValuesMat globValuesClass;
  13.     public MathClass() {
  14.         globValuesClass = new GlobValuesMat();
  15.     }
  16.     public UInt32 Add(UInt32 num1, UInt32 num2)
  17.     {
  18.         globValuesClass.rememberOperationData = num1 + num2;
  19.         return globValuesClass.rememberOperationData;
  20.     }
  21.     public UInt32 Subtraction(UInt32 num1, UInt32 num2)
  22.     {
  23.         if(checkIfOperationCanBeContinue(num1, num2) == false)
  24.         {
  25.             return 0x00;
  26.         }
  27.         globValuesClass.rememberOperationData = num1 - num2;
  28.         return globValuesClass.rememberOperationData;
  29.     }
  30.     public UInt32 GetLastOperationValues()
  31.     {
  32.         return globValuesClass.rememberOperationData;
  33.     }
  34.     private bool checkIfOperationCanBeContinue(UInt32 num1, UInt32 num2)
  35.     {
  36.         if(num1 < num2)
  37.         {
  38.             return false;
  39.         }
  40.         return true;
  41.     }
  42. }

W tym przypadku ustawiamy wartość zmiennej globalnej zawierający ostatni wynik wykonywanej operacji. Jeśli mamy do czynienia z takim przypadkiem to oprócz sprawdzenia odpowiedzi od funkcji należy sprawdzić czy odpowiednia wartość globalna została do niej wpisana.

Przykładowy test wywołania funkcji dodającej wygląda następująco:

  1. [Test]
  2. public void MathClass_Add_AddOperationResult()
  3. {
  4.     testAppWpf.IMathClass mathClass = new testAppWpf.MathClass();
  5.     UInt32 returnValue = mathClass.Add(12);
  6.     UInt32 expectedValue = 3;
  7.     Assert.AreEqual(expectedValue, returnValue);
  8.     Assert.AreEqual(expectedValue, mathClass.GetLastOperationValues());
  9. }

Testy funkcji void opierają się na sprawdzeniu czy dany element został ustawiony bądź uruchomiony.

Teraz sprawdzenie czy dana funkcja wywołuje odpowiedni wyjątek. Poniżej przykładowa klasa testowa, której zadanie polega na zwróceniu odpowiedniego znaku po przekazaniu jego pozycji w tablicy.

  1. public class GetCharacter
  2. {
  3.     char[] charArray = { 'a''b''c''d''e''f''g''h''i''j' };
  4.  
  5.     public GetCharacter()
  6.     { }
  7.  
  8.     public char GetCharacterWithIndex(int index)
  9.     {
  10.         if (index < 0 || index >= charArray.Length)
  11.         {
  12.             throw new IndexOutOfRangeException();
  13.         }
  14.         return charArray[index];
  15.     }
  16. }

Przykładowy test dla sprawdzenia zwrócenia poprawnej wartości:

  1. [Test]
  2. public void GetCharacterWithIndex_TestEnableException_ExceptionOccureWrongIndexValue()
  3. {
  4.     GetCharacter getCharacterClass = new GetCharacter();
  5.     int outOfRangeValue = 20;
  6.     bool operationStatus = false;
  7.  
  8.     try
  9.     {
  10.         getCharacterClass.GetCharacterWithIndex(outOfRangeValue);
  11.     }
  12.     catch(IndexOutOfRangeException)
  13.     {
  14.         operationStatus = true;
  15.     }
  16.     Assert.IsTrue(operationStatus);
  17. }

Jeden z przypadków opiera się na wykorzystaniu bloków try catch. Po wyłapaniu wyjątku zmieniam wartość zmiennej bool i sprawdzam czy udało się wywołać wyjątek.

Drugi przypadek wywoływania wyjątku:

  1. public class StringTestClass
  2. {
  3.     string _wholestring;
  4.  
  5.     public StringTestClass()
  6.     {
  7.         _wholestring = "";
  8.     }
  9.  
  10.     public string Wholestring
  11.     {
  12.         get { return _wholestring; }
  13.     }
  14.  
  15.  
  16.     public void CheckParam(string data)
  17.     {
  18.         if (data == null)
  19.         {
  20.             throw new ArgumentNullException();
  21.         }
  22.         else
  23.         {
  24.             _wholestring += data + "&*&";
  25.         }
  26.     }
  27. }

Poniżej test sprawdzający jedynie wyzwolenie wyjątku ArgumentNullException:

  1. [Test]
  2. public void CheckParam_ThrowsException_ArgumentNullException()
  3. {
  4.     StringTestClass stringTestClass = new StringTestClass();
  5.     string passedString = null;
  6.  
  7.     Assert.That(() => stringTestClass.CheckParam(passedString),
  8.                                     Throws.ArgumentNullException);
  9. }

Teraz opis sposobu testowania wyzwolenia zdarzenia przez funkcję. Poniżej krótka klasa testowa:

  1. public class ExampleClassRaiseEvent
  2. {
  3.     int _mathVal;
  4.     public event EventHandler<Guid> _EventHandler;
  5.  
  6.     public ExampleClassRaiseEvent() { }
  7.  
  8.     public int MathVal
  9.     {
  10.         get { return _mathVal; }
  11.     }
  12.  
  13.     public void ExampleFunction(int val)
  14.     {
  15.         if(val == 1)
  16.         {
  17.             _mathVal++;
  18.         }
  19.         else if(val == 2)
  20.         {
  21.             _mathVal += 2;
  22.         }
  23.         else
  24.         {
  25.             _EventHandler?.Invoke(this, Guid.NewGuid());
  26.         }
  27.     }
  28. }

Funkcja testowa sprawdzająca czy zdarzenie zostało wywołane:

  1. [Test]
  2. public void ExampleFunction_CheckIfEvantRises()
  3. {
  4.     ExampleClassRaiseEvent exampleClassRaiseEvent = new ExampleClassRaiseEvent();
  5.     var id = Guid.Empty;
  6.  
  7.     exampleClassRaiseEvent._EventHandler += (sender, args) => { id = args; };
  8.     exampleClassRaiseEvent.ExampleFunction(3);
  9.  
  10.     Assert.That(id, Is.Not.EqualTo(Guid.Empty));
  11. }

W tym przypadku sprawdzenie polega na przetestowaniu czy do zmiennej id zostały przypisane jakieś informacje. Jeśli tak to wartość będzie się różniła od wcześniej przypisanej do niej zmiennej.

Teraz drugi przypadek wywołania zdarzenia w aplikacji. Poniżej przykładowa klasa testowa:

  1. public class TestEvent_SecondClass
  2. {
  3.     public event EventHandler<EventFunction> ExampleRiseEvent;
  4.     string _eventMsg;
  5.  
  6.     public TestEvent_SecondClass()
  7.     {
  8.         ExampleRiseEvent += a_EventFunction;
  9.     }
  10.  
  11.     public string EventMsg
  12.     {
  13.         get { return _eventMsg; }
  14.     }
  15.  
  16.     public int Add(int x)
  17.     {
  18.         if(== 1)      { return 84; }
  19.         else if(== 2) { return 97; }
  20.         else if(ExampleRiseEvent != null)
  21.         {
  22.             ExampleRiseEvent(thisnew EventFunction(Convert.ToString(x)));
  23.         }
  24.  
  25.         return 0;
  26.     }
  27.  
  28.     void a_EventFunction(object sender, EventFunction e)
  29.     {
  30.         _eventMsg = e.ErrorString;
  31.     }
  32. }
  33.  
  34. public class EventFunction : EventArgs
  35. {
  36.     public string ErrorString { get; set; }
  37.  
  38.     public EventFunction(string newErrorMsg)
  39.     {
  40.         ErrorString = "Error " + newErrorMsg;
  41.     }
  42. }

Przykładowy test sprawdzający czy dana w zmiennej globalnej została zapisana oraz czy dane zdarzenie zostało wywołane:

  1. [Test]
  2. public void ExampleFunction_CheckIfEvantRises()
  3. {
  4.     TestEvent_SecondClass testEvent_SecondClass = new TestEvent_SecondClass();
  5.     var id = EventArgs.Empty;
  6.  
  7.     testEvent_SecondClass.ExampleRiseEvent += (sender, args) => { id = args; };
  8.  
  9.     testEvent_SecondClass.Add(3);
  10.  
  11.     StringAssert.Contains("Error", testEvent_SecondClass.EventMsg);
  12.     Assert.That(id, Is.Not.EqualTo(EventArgs.Empty));
  13. }