środa, 12 grudnia 2012

Jak przyspieszyć kodowanie cz.4 (testy sparametryzowane)

To, co mi się najbardziej podoba w NUnit to 'sparametryzowane testy'. Najlepiej na przykładzie będę mógł pokazać ci piękno tego rozwiązania. Dajmy na to, że mamy klasę z 'skomplikowaną' logiką.
public class Sum
{
 public int Value1 { get; set; }
 public int Value2 { get; set; }

 public int Result
 {
    get { return Value1 + Value2; }
 }
}
Klasa Sum w polu Result zwraca sumę pól Value1 i Value2. Użycie tej klasy wygląda następująco:
var sum =  new Sum { Value1 = value1, Value2 = value2 };
sum.Result;
Stwórzmy do niego test sprawdzający działanie przykładowych liczb, dodawanie elementu neutralnego (zero) oraz dodawanie liczby przeciwnej.
        [Test]
        public void It_should_return_sum_3()
        {
            int value1 = 1;
            int value2 = 2;
            var sum = new Sum { Value1 = value1, Value2 = value2 };
            Assert.AreEqual(3, sum.Result);
        }

        [Test]
        public void It_should_return_0_because_number_of_opposing()
        {
            int value1 = 1;
            int value2 = -1;
            var sum = new Sum { Value1 = value1, Value2 = value2 };
            Assert.AreEqual(0, sum.Result);
        }

        [Test]
        public void It_should_return_0_because_adding_neutral_nuber()
        {
            int value1 = 2;
            int value2 = 0;
            var sum = new Sum { Value1 = value1, Value2 = value2 };
            Assert.AreEqual(2, sum.Result);
        }
Mamy 3 metody, które testują 3 różne przypadki. Od razu widać, że kod się powtarza, więc chcemy zredukować powtarzający się kod w tych testach. Możemy zastosować atrybut TestCase. W tym atrybucie podajemy wartości obiektów z jakimi ma być wywołana metoda. Przykład jest poniżej:
        [TestCase(1, 2, 3)]
        [TestCase(2, 0, 2)]
        [TestCase(1, -1, 0, TestName = "Because number of opposing")]
        public void It_should_return_sum(int value1, int value2, int expected)
        {
            var sum = new Sum { Value1 = value1, Value2 = value2 };
            Assert.AreEqual(expected, sum.Result);
        }
Dokładnie to mi się najbardziej podoba w NUnit. Mamy przetestowane 3 różne przypadki użycia i napisało się tylko jedną metodę. W tej metodzie 2 pierwsze argumenty atrybutu są jako dane wejściowe naszej operacji, a 3 argument jest jako wynik tej operacji. Dla poszczególnych przypadków testowych (TestCase), można jeszcze określić nazwę testu, opis, jaki wyjątek ma być wyrzucony oraz jaka wartość ma być zwracana przez tą metody. Możemy wykorzystać ten ostatni parametr.

        [TestCase(1, 2, Result = 3)]
        [TestCase(1, -1, Result = 0, TestName = "It should return 0")]
        [TestCase(2, 0, Result = 2, TestName = "Adding zero")]
        public int It_should_return_sum2(int value1, int value2)
        {
            var sum = new Sum { Value1 = value1, Value2 = value2 };
            return sum.Result;
        }
Przekształcenie bardzo przypomina 'normalną' (bez aserci) metodę, ale z atrybutami. Mam diaboliczny pomysł. Parametr Result możemy dodać do normalnej metod statycznej. W taki sposób można mieć kod implementacyjny i testy w jednym projekcie - it's crazy.

    public class SumFactory
    {
        [TestCase(1, 2,  Result = 3)]
        [TestCase(1, -1, Result = 0, TestName = "It should return 0")]
        [TestCase(2, 0,  Result = 2, TestName = "Adding zero")]
        public int Sum(int value1, int value2)
        {
            return value1 + value2;
        }
    }
To jest pierwszy przykład jak możemy wykorzystać TestCase - drugi przykład jest dla TestFixture. Napiszmy generyczną klasę, która będzie dodać 2 zmienne:

    public class GenericSum<T>
    {
        public T Value1 { get; set; }
        public T Value2 { get; set; }

        public T SumValue
        {
            get { return (dynamic)Value1 + (dynamic)Value2; }
        }
    }
I chcielibyśmy przetestować dodawanie domyślnych wartości dla zmiennych typu int i double.
    [TestFixture(typeof(int))]
    [TestFixture(typeof(double))]
    public class GenericTestNUnit<T>
    {
        [Test]
        public void It_should_return_default_value()
        {
            var sum = new GenericSum<T> { Value1 = default(T), Value2 = default(T)};
            Assert.AreEqual(default(T), sum.SumValue);
        }
    }
Argument w atrybucie TestFixture odnosi się do typu T. Takie sparametryzowane testy mają zastosowanie w testowaniu klas dziedziczących.

    public abstract class AsBiOperation
    {
        public int Value1 { get; set; }
        public int Value2 { get; set; }
        public abstract int Result { get; }
    }
    public class SumOperation : AsBiOperation
    {
        public override int Result
        {
            get { return Value1 + Value2; }
        }
    }
    public class MultiplicationOperation : AsBiOperation
    {
        public override int Result
        {
            get { return Value1*Value2; }
        }
    }
Do stworzonej klasy abstrakcyjną i napiszemy test dla wszystkich klas dziedziczących po niej. Dodatkowo mamy możliwość wprowadzenia przykładowych wartości.

    [TestFixture(1,2,3,     TypeArgs = new[] { typeof(SumOperation)} )]
    [TestFixture(0, 0,0,    TypeArgs = new[] { typeof(SumOperation) })]
    [TestFixture(-1, 1, 0,  TypeArgs = new[] { typeof(SumOperation) })]

    [TestFixture(1, 0, 0,   TypeArgs = new[] { typeof(MultiplicationOperation) })]
    [TestFixture(1, 5, 5,   TypeArgs = new[] { typeof(MultiplicationOperation) })]
    [TestFixture(5, 5, 25,  TypeArgs = new[] { typeof(MultiplicationOperation) })]    
    public class AsBiOperationTests<T>
        where T:AsBiOperation, new()
    {
        T operation;
        int _value2;
        int _value1;
        int _result;

        public AsBiOperationTests(int value1, int value2, int result)
        {
            _result = result;
            _value1 = value1;
            _value2 = value2;
        }

        [SetUp]
        public void SetUp()
        {
            operation = new T();
        }


        [Test]
        public void It_should_calculate()
        {
            operation.Value1 = _value1;
            operation.Value2 = _value2;
            Assert.AreEqual(_result,operation.Result);
        }
        
    }

Testy sparametryzowane to fantastyczne rozwiązanie, aby mniej kodować, mieć większe pokrycie kodu i móc więcej przetestować przypadków. Bardzo żałuje, że w MSTest jeszcze nie ma takiego featura (chociaż nie bezpośrednio). Jednak mam nadzieję, że w bliskiej przyszłości coś się z tym zmieni. Oprócz NUnit'a sparametryzowane test są w MBUnit.

Brak komentarzy:

Prześlij komentarz