Jakiś czas temu pisałem o
TestCase w NUnit. Czasami takie rozwiązanie może zaoszczędzić troszkę czasu. Zauważyłem, że bardzo często testuje metody dla takich samych danych wejściowych. Dla parametru typu string sprawdzam jak metoda zadziała dla null, pustego stringa i stringa z białym znakiem.
public class SimpleTestClass
{
[TestCase(null)]
[TestCase("")]
[TestCase(" ")]
public void SimpleTestCase(string parametr)
{
Assert.IsTrue(string.IsNullOrWhiteSpace(parametr));
}
}
A gdyby tak trzymać kolekcję przypadków testowych w oddzielnym miejscu? Mamy taką możliwość - możemy użyć TestCaseSource. Niestety wszystkie metody muszą być statyczne.
[TestCaseSource(typeof(StringTestCaseSource), "NullAndWhiteSpaceStrings")]
public void It_should_return_true(string arg1)
{
Assert.IsTrue(string.IsNullOrWhiteSpace(arg1));
}
Implementacja metody, która zwraca nam potrzebne dane testowe:
public static class StringTestCaseSource
{
public static IEnumerable<TestCaseData> NullAndWhiteSpaceStrings
{
get
{
yield return new TestCaseData(null);
yield return new TestCaseData("");
yield return new TestCaseData(" ");
}
}
}
Inny przypadek użycia - wartości enumów. Chcielibyśmy uruchomić test dla wszystkich przypadków enuma. Możemy podawać nazwy elementów w enumie jak i jego wartości. Dla enuma:
public enum MoneyDirection
{
Pay,
Receive,
}
Możemy napisać proste testy:
[TestCaseSource(typeof(EnumTestCaseSource<MoneyDirection>), "Names")]
public void It_should_parse_moneyDirection_string(string nameOfEnums)
{
var objectEnum = Enum.Parse(typeof (MoneyDirection), nameOfEnums);
Assert.IsNotNull(objectEnum);
}
[TestCaseSource(typeof(EnumTestCaseSource<MoneyDirection>), "Values")]
public void It_should_parse_moneyDirection_type(MoneyDirection valuesOfEnums)
{
Assert.IsTrue(Enum.IsDefined(typeof(MoneyDirection), valuesOfEnums));
}
A sama implementacja metod jest następująca:
public static IEnumerable<TestCaseData> Names
{
get
{
return Enum.GetNames(typeof(T))
.Select(x => new TestCaseData(x));
}
}
public static IEnumerable<TestCaseData> Values
{
get
{
return Enum.GetValues(typeof(T))
.Cast<T>()
.ToTestCaseData(); //with extension method
}
}
Przypuśćmy, że mamy klasę, która zawiera statyczne formaty dat:
public class DateTimeRequiredFormats
{
public const string Req1 = "yyyyMMdd"; //const is
public readonly static string Req2 = "yyyy MM dd";
public static string Req3 = "yyyy MM dd";
public string Req4 = ""; //not used because not static
////////////
public string Req5 { get { return "a"; } }
public string Req6 { get; set; }
public DateTimeRequiredFormats(string req6)
{
Req6 = req6;
}
}
Aby sprawdzić działanie statycznych formatów (Req1, Req2, Req3) wystarczyło wywołać:
[TestCaseSource(typeof(FieldTestCaseSource<DateTimeRequiredFormats>), "Static")]
public void It_should_parse_dateTimeFormat(object arg1)
{
var dateTime = DateTime.ParseExact("20131029", arg1.ToString(), null,
DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AllowInnerWhite );
Assert.AreEqual(new DateTime(2013,10,29), dateTime);
}
A metoda, która zwraca te zmienne ma implementację:
public static class FieldTestCaseSource<T>
{
public static IEnumerable<TestCaseData> Static
{
get
{
var propertyInfos = typeof(T).GetFields()
.Where(x=>x.IsStatic)
.Select(x=>x.GetValue(null))
;
return propertyInfos.ToTestCaseData();
}
}
}
A co z metoda? Tutaj już jest troszkę trudniejsza sytuacja. Oprócz klasy z metodami to potrzebny będzie nam obiekt, który będzie mógł uruchomić metodę. Poniżej klasa z metodami do uruchamiania w teście:
public class InformationGetter
{
public static int GetSuperNumber()
{
return 77;
}
public static int GetNotSuperNumber()
{
return 7;
}
public string GetCharAt(int indexNumber)
{
return _specialString[indexNumber]
.ToString(CultureInfo.InvariantCulture);
}
private string _specialString;
public InformationGetter(string specialString)
{
_specialString = specialString;
}
public void ConsoleWrite()
{
Console.WriteLine("ConsoleWirte:" + _specialString);
}
}
Poniżej sam test. Zauważ, że podany tej tutaj typ zwracany przez metodę (int).
[TestCaseSource(typeof(MethodTestCaseSource<InformationGetter,int>), "Static")]
public void It_should_return_int_bigger_than_0(MethodInvoker<int> staticMethodToInvoke)
{
Assert.Greater(staticMethodToInvoke.Invoke(), 1);
}
public static class MethodTestCaseSource<T, TReturn>
{
public static IEnumerable<TestCaseData> Static
{
get
{
var methodInfos= MethodTestCaseSource<T>.GetStaticMethods()
.Where(x => x.ReturnType == typeof (TReturn));
var methodInvokers = MethodInvoker<TReturn>.ToInvoker(methodInfos);
return methodInvokers
.ToTestCaseData(x=>x.Method.Name);
}
}
public static IEnumerable<TestCaseData> NonStatic
{
get
{
var methods = MethodTestCaseSource<T>.GetInstanceMethods()
.Where(x => x.ReturnType == typeof(T));
var methodInvokers = MethodInvoker.ToInvoker(methods);
return methodInvokers
.ToTestCaseData(x => x.Method.Name);
}
}
}
Klasa MethodInvoker wywołuje daną metodę. Klasa ma dwie postacie (bez i z użyciem generyków). Jedna i druga metoda ma podobną implementację.
public class MethodInvoker<TReturn> : MethodInvoker
{
public MethodInvoker(MethodInfo methodInfo):base(methodInfo){}
public TReturn Invoke(object obj=null, object[] parameters=null)
{
return (TReturn)Method.Invoke(obj, parameters);
}
public static IEnumerable<MethodInvoker<TReturn>> ToInvoker(IEnumerable<MethodInfo> methodInfos)
{
return methodInfos.Select(x => new MethodInvoker<TReturn>(x));
}
}
Dzięki MethodInvoker możemy też przesyłać metody, które nie są statyczne. Wystarczy dla metody Invoke podać obiekt:
[TestCaseSource(typeof(MethodTestCaseSource<InformationGetter, string>), "NonStatic")]
public void It_should_return_first_char(MethodInvoker<string> nonStaticMethodToInvoke)
{
InformationGetter getter=new InformationGetter("Abc");
var fistCharString = nonStaticMethodToInvoke.Invoke(getter, new object[] {0});
StringAssert.AreEqualIgnoringCase("a", fistCharString);
}
Kod źródłowy projektu jest udostępniony na
GitHubie. Coś może dodam do tych kolekcji testcesów np. wygenerowane losowe stringi, dane z plików resx, najważniejsze daty w DateTime, adresy url i mail.
Możesz też zainstalować sobie
paczkę nugetową. Wystarczy wpisać:
>Install-Package Nunit.Framework.TestCaseStorage