среда, 26 октября 2011 г.

Полезный класс для тестов

Часто бывает так, что для тестирования класса нужен набор объектов, заполненных произвольными или предопределенными данными. Но заполнять эти объекты - дело муторное. Ниже простой в исполнении и использовании класс, предназначенный специально для заполнения объектов.

Предположим, что у нас есть интерфейс
Code Snippet
  1. public interface IPerson
  2. {
  3.     string Name { get; }
  4.     int Age { get; }
  5.  
  6.     IEnumerable<IPerson> Friends { get; }
  7.     void AddFriend(IPerson person);
  8.     void RemoveFriend(IPerson person);
  9. }
И, соответственно, класс
Code Snippet
  1. public class Programmer : IPerson
  2. {
  3.     private readonly List<IPerson> _friends;
  4.  
  5.     public Programmer()
  6.     {
  7.         _friends = new List<IPerson>();
  8.     }
  9.  
  10.     public string Name { get; set; }
  11.     public int Age { get; set; }
  12.     public String Email { get; set; }
  13.  
  14.     public IEnumerable<IPerson> Friends
  15.     {
  16.         get { return _friends; }
  17.     }
  18.  
  19.     public void AddFriend(IPerson person)
  20.     {
  21.         _friends.Add(person);
  22.     }
  23.  
  24.     public void RemoveFriend(IPerson person)
  25.     {
  26.         _friends.Remove(person);
  27.     }
  28. }
Пусть наша задача - протестировать какое-либо действие над нашим объектом
Code Snippet
  1. public class SomeAction
  2. {
  3.     public IPerson Action(IPerson person)
  4.     {
  5.         // Что то происходит
  6.         return person;
  7.     }
  8. }
Теперь поставим задачу: пусть, для тестирования этого действия, мы должны передать ему объект IPerson, который будет заполнен адекватными данными. То есть Имя не пустое, возраст неотрицательный, есть хотя бы 1 друг, данные которого тоже заполнены, но нет друзей, например. Вот пример теста:
Code Snippet
  1. [TestClass]
  2. public class SampleTest
  3. {
  4.     [TestMethod]
  5.     public void Test()
  6.     {
  7.         var programmer = new Programmer
  8.         {
  9.             Age = 18,
  10.             Email = "email@email.em",
  11.             Name = "Name"
  12.         };
  13.         programmer.AddFriend(new Programmer
  14.         {
  15.             Age = 18,
  16.             Email = "email@email.em",
  17.             Name = "Name"
  18.         });
  19.  
  20.         var act = new SomeAction();
  21.  
  22.         var result = act.Action(programmer);
  23.         // Гора Assert'ов
  24.     }
  25. }
Думаю, недостатки этого подхода очевидны. А если надо заполнить не 3 поля, а 30? А если поля в класс добавляются уже после написания теста? так неохота править такие тесты... Ниже представлен целый класс, который упрощает процесс установки значений полей произвольными значениями
Code Snippet
  1. public static class TestDataSetter
  2. {
  3.     /// <summary>
  4.     /// Устанавливаем значение объекта
  5.     /// </summary>
  6.     public static T SetData<T, TK>(this T source, TK value) where T : class
  7.     {
  8.         var type = typeof(T);
  9.         var tktype = typeof(TK);
  10.         SetData(type, tktype, source, value);
  11.         return source;
  12.     }
  13.  
  14.     /// <summary>
  15.     /// Используем действие для установки значения объекта или поля
  16.     /// </summary>
  17.     public static T SetData<T, TK>(this T source, Func<T, TK> action) where T : class
  18.     {
  19.         action(source);
  20.         return source;
  21.     }
  22.  
  23.     /// <summary>
  24.     /// Используем действие, возвращающее Void для установки значения объекта или поля
  25.     /// </summary>
  26.     public static T SetData<T>(this T source, Action<T> action) where T : class
  27.     {
  28.         action(source);
  29.         return source;
  30.     }
  31.  
  32.     /// <summary>
  33.     /// Устанавливаем значения простых полей классов
  34.     /// </summary>
  35.     public static T SetData<T>(this T source) where T : class
  36.     {
  37.         var type = typeof(T);
  38.  
  39.         SetStringData(type, source);
  40.  
  41.         SetData(source, 0);
  42.         SetData(source, (int?)0);
  43.         SetData(source, false);
  44.         SetData(source, (bool?)false);
  45.         SetData(source, Guid.Empty);
  46.         SetData(source, (Guid?)Guid.Empty);
  47.         SetData(source, new DateTime(2011, 4, 1));
  48.         SetData(source, (DateTime?)new DateTime(2011, 4, 1));
  49.         return source;
  50.     }
  51.  
  52.     /// <summary>
  53.     /// Устанавливаем строковые значения класса
  54.     /// </summary>
  55.     private static void SetStringData(Type sourceType, object source)
  56.     {
  57.         var valueType = typeof(String);
  58.         var props = sourceType.GetProperties();
  59.         foreach (var propertyInfo in props)
  60.         {
  61.             if (propertyInfo.PropertyType == valueType)
  62.                 propertyInfo.SetValue(source, propertyInfo.Name, null);
  63.         }
  64.         if (sourceType.BaseType != null)
  65.         {
  66.             SetStringData(sourceType.BaseType, source);
  67.         }
  68.     }
  69.  
  70.     /// <summary>
  71.     /// Проход по всем полям и установка их значений
  72.     /// </summary>
  73.     private static void SetData(Type sourceType, Type valueType, object source, object value)
  74.     {
  75.         var props = sourceType.GetProperties();
  76.         foreach (var propertyInfo in props)
  77.         {
  78.             if (propertyInfo.PropertyType == valueType)
  79.                 propertyInfo.SetValue(source, value, null);
  80.         }
  81.         if (sourceType.BaseType != null)
  82.         {
  83.             SetData(sourceType.BaseType, valueType, source, value);
  84.         }
  85.     }
  86. }
Теперь рассмотрим самое простой использование нашего помощника:
 Как видно, заполнены текстовые поля. Также целые числа установлены в 0 - допустим, это нас не устраивает. Поэтому можно сразу тут же установить возраст. Это можно сделать 2мя способами: установить все поля типа int в определенное значение, либо установить только поле возраста. Вот оба варианта использования:
 Теперь нам нужно добавить хотя бы 1 друга нашему программисту. Делается очень просто.
Таким образом, наш тест можно переписать вот так:
Code Snippet
  1. [TestClass]
  2. public class SampleTest
  3. {
  4.     [TestMethod]
  5.     public void Test2()
  6.     {
  7.         var programmer = new Programmer()
  8.           .SetData()
  9.           .SetData(x => x.Age = 18)
  10.           .SetData(x => x.AddFriend(
  11.             new Programmer().SetData().SetData(x2 => x2.Age = 18)
  12.             ));
  13.  
  14.         var act = new SomeAction();
  15.         var result = act.Action(programmer);
  16.         // Гора Assert'ов
  17.     }
  18. }
Как видно, он стал меньше за счет уменьшения кода при инициализации объектов для теста.

2 комментария: