Подделываем данные, как профессионал

Постановка

Рассмотрим небольшой класс Student

    public class Student
    {
       public string Name { get; set; }
       public int Age { get; set; }
       public bool HasLicense { get; set; }
    }

и класс Security, занимающийся проверкой лицензий

    public class Security
    {
       public bool CheckLicense(Student student)
       {
          return student.HasLicense;
       }
    }

Нам необходимо протестировать функциональность метода CheckLicense.

Стандартный тест

Основываясь на опыте предыдущей статьи, мы могли бы написать что-то в духе

    [Fact]
    public void CheckLicense_Success()
    {
       var student = FakeFactory.Fixture.Create<Student>();
     //
       var result = security.CheckLicense(student);
     //
       Assert.True(result == student.HasLicense);
    }

Выглядит неплохо, но что если усложнить ситуацию и включить в схему некий сервис, выдающий статус студента в соответствии с датой запроса?

    public interface IStudentLicenseStore
    {
       bool GetLicense(string name, DateTime date);
    }
    
    public class Security
    {
       private readonly IStudentLicenseStore _store;
   
       public Security(IStudentLicenseStore store)
       {
          _store = store;
       }
       public bool CheckLicense(Student student, DateTime date)
       {
          return _store.GetLicense(student.Name, date);
       }
    }

Конечно, bool здесь выглядит странно, но для наших целей сойдет и такая упрощенная модель. Как же изменится наш тест?

   [Fact]
    public void CheckLicense_Success()
    {
       var student = FakeFactory.Fixture.Create<Student>();
       var serviceResult = FakeFactory.Fixture.Create<bool>();
       var date = FakeFactory.Fixture.Create<DateTime>();
       _store.Setup(s => s.GetLicense(student.Name, date)).Returns(serviceResult);
       //
       var result = security.CheckLicense(student, date);
       //
       Assert.True(result == serviceResult);
    }

Вводных данных все больше и тест становится громоздким, хотя, в действительности, строчки с объявлением переменных не несут никакой смысловой нагрузки. Можно ли упростить этот код?

AutoMoqDataAttribute

На выручку нам могли бы придти атрибуты Theory и AutoData, они сами по себе могут обеспечить подделку всех нужных нам данных, однако в этом случае мы теряем возможность использовать класс FakeFactory и настраивать подделки по своему усмотрению. Учитывая то, что эти настройки нам очень пригодятся в будущих статьях, отказываться от них явно не стоит)

Создадим в тестовом проекте класс AutoMoqDataAttribute

    public class AutoMoqDataAttribute : AutoDataAttribute
    {
        public AutoMoqDataAttribute() : base(FakeFactory.Fixture)
        {
        }
    }

Как видно, этот класс является легковесной оберткой над AutoDataAttribute и единственная его задача - передать наши настройки подделок, что очень пригодится нам в будущем.

Изменим наш тест

    [Theory, AutoMoqData]
    public void CheckLicense_Success(Student student, bool serviceResult, DateTime date)
    {
       _store.Setup(s => s.GetLicense(student.Name, date)).Returns(serviceResult);
       //
       var result = security.CheckLicense(student, date);
       //
       Assert.True(result == serviceResult);
    }

Тест выглядит гораздо более опрятно и не перегружен несущественными данными, победа?

InlineAutoDataAttribute

При написании тестов иногда возникают ситуации, когда автоматически сформированные подделки не соответствуют требованиям приложения, так у Student может быть поле Phone, которое является значимым для тестируемого метода. В обычной ситуации мы могли бы использовать InlineData, но AutoMoqDataAttribute блокирует использование InlineData и не позволит использовать указанные значения.

Для того, чтобы решить эту проблему добавим еще один класс поддержки.

    public class InlineAutoMoqDataAttribute : InlineAutoDataAttribute
    {
        public InlineAutoMoqDataAttribute(params object[] objects) : base(new AutoMoqDataAttribute(), objects)
        {
        }
    }

И снова обертка над базовым классом AutoFixture, для нас важны два момента.

  1. Передача AutoMoqDataAttribute позволит использовать искомый экземпляр FakeFactory.
  2. Передача параметров InlineData. При таком подходе будет выполнен проход по всем входящим параметрам теста; нулевой параметр будет взят из нулевого индекса objects, первый из первого и т.д. Если objects закончится, а параметры все еще требуются, вступит в силу AutoMoqDataAttribute и будет формировать рандомные подделки.

В итоге наш тест будет выглядеть как-то так:

    [Theory]
    [InlineAutoMoqData("123456789")]
    public void CheckLicense_Success(string phone, Student student, bool serviceResult, DateTime date)
    {
       _store.Setup(s => s.GetLicense(student.Name, date)).Returns(serviceResult);
       //
       var result = security.CheckLicense(student, date);
       //
       Assert.True(result == serviceResult);
    }

Обратите внимание, что AutoMoqDataAttribute в данном случае не используется напрямую.

Вывод

Использование двух описанных классов должно, по моему мнению, войти в стандарт организации тестирующего кода, способствуя улучшению читаемости и упрощению написания тестов.