Unit тесты на практике

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

Сразу оговорюсь, что примеры приводятся применительно к языку C# и платформе .NET. Соответственно, в других языках/платформах подходы и реализации могут отличаться.

Итак…

Какими должны быть модульные тесты?

Помимо того, что модульные тесты должны соответствовать функциональности программного продукта, основное требование — скорость работы. Если после запуска набора тестов разработчик может сделать перерыв (в моём случае на перекур), то подобные запуски будут происходить всё реже и реже (опять же, в моём случае, из-за опасения получить передозировку никотином). В результате может получиться так, что модульные тесты вообще не будут запускаться и, как следствие, потеряется смысл их написания. Программист должен иметь возможность запустить весь набор тестов в любой момент времени. И этот набор должен выполниться настолько быстро, насколько это возможно.

Какие требования необходимо соблюсти для того, чтобы обеспечить скорость выполнения модульных тестов?

Тесты должны быть небольшими

В идеальном случае — одно утверждение (assert) на тест. Чем меньше кусочек функциональности, покрываемый модульным тестом, тем быстрее тест будет выполняться.

Кстати, на тему оформления. Мне очень нравится подход, который формулируется как «arrange-act-assert».
Суть его заключается в том, чтобы в модульном тесте чётко определить предусловия (инициализация тестовых данных,
предварительные установки), действие (собственно то, что тестируется) и постусловия (что должно быть в
результате выполнения действия). Подобное оформление повышает читаемость теста и облегчает его
использование в качестве документации к тестируемой функциональности.

Если в разработке используется ReSharper от JetBrains, то очень удобно настроить template, с помощью которого будет создаваться заготовка для тестового случая. Например, template может выглядеть вот так:

[Test]
public void Test_$METHOD_NAME$()
{
    //arrange
    $END$

    //act

    //assert
    Assert.Fail("Not implemented");
}


И тогда тест, оформленный подобным образом, может выглядеть примерно так (все имена вымышленные, совпадения случайны):

[Test]
public void Test_ForbiddenForPackageChunkWhenPackageNotFound()
{
    //arrange
    var packagesRepositoryMock = _mocks.Create<IPackagesRepository>();
    packagesRepositoryMock
        .Setup(r => r.FindPackageAsync(_packageId))
        .Returns(Task<DatabasePackage>.Factory.StartNew(() => null));
    Register(packagesRepositoryMock.Object);

    //act
    var message = PostChunkToServer(new byte[] { 1, 2, 3 });

    //assert
    _mocks.VerifyAll();

    Assert.That(message.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
}


Тесты должны быть изолированы от окружения (БД, сеть, файловая система)

Этот пункт, наверное, самый спорный из всех. Зачастую в литературе рассматриваются примеры использования TDD на таких задачах как разработка калькулятора или телефонного справочника. Понятно, что эти задачи оторваны от реальности и разработчик, возвращаясь в свой проект, не знает, как применить полученные знания и навыки в повседневной работе. Простейшие случаи, похожие на пресловутый калькулятор, покрываются модульными тестами, а остальная часть функциональности разрабатывается на прежних рельсах. В итоге пропадает понимание того, зачем тратить время на модульные тесты, если простой код можно и так отладить, а сложный всё равно тестами не покрывается.

На самом же деле нет ничего страшного, если в модульных тестах будет использоваться база данных или файловая система. Так, по крайней мере, можно быть уверенным в работоспособности той функциональности, которая по большому счёту и составляет ядро системы. Вопрос заключается только в том, как использовать внешнее окружение наиболее эффективным способом, соблюдая баланс между изолированностью тестов и скоростью их выполнения?

Случай 1. Слой доступа к данным (MS SQL Server)

Если в разработке проекта используется MS SQL сервер, то ответом на этот вопрос может быть использование установленного экземпляра MS SQL сервер (Express, Enterprise или Developer Edition) для разворачивания тестовой базы данных. Подобную базу данных можно создать с помощью стандартных механизмов, используемых в MS SQL Management Studio и поместить её в проект с модульными тестами. Общий подход к использованию такой базы данных заключается в разворачивании тестовой БД перед выполнением теста (например, в методе, отмеченном атрибутом SetUp в случае использования NUnit), наполнении БД тестовыми данными и проверками функциональности репозиториев или шлюзов на этих, заведомо известных, тестовых данных. Причём разворачиваться тестовая БД может как на жёстком диске, так и в памяти, используя приложения, создающие и управляющие RAM диском. Допустим, в проекте, над которым я работаю в данное время, используется приложение SoftPerfect RAM Disk. Использование RAM диска в модульных тестах позволяет снизить задержки, возникающие при операциях ввода/вывода, которые возникали бы при разворачивании тестовой БД на жёстком диске. Конечно, данный подход не идеален, так как требует внесения в окружение разработчика стороннего ПО. С другой стороны, если учесть, что среда для разработки разворачивается, как правило, один раз (ну, или достаточно редко), то это требование не кажется таким уж обременяющим. Да и выигрыш от использования такого подхода достаточно заманчив, ведь появляется возможность контролировать корректность работы одного из важнейших слоёв системы.

Кстати, если есть возможность использовать в модульных тестах LINQ2SQL и SMO для MS SQL Server, то можно воспользоваться следующим базовым классом для тестирования слоя доступа к данным:

Код
public abstract class DatabaseUnitTest<TContext> where TContext : DataContext
{
    [TestFixtureSetUp]
    public void FixtureSetUp()
    {
        CreateFolderForTempDatabase();
    }

    [SetUp]
    public void BeforeTestExecuting()
    {
        RestoreDatabaseFromOriginal();
        RecreateContext();		
    }

    [TestFixtureTearDown]
    public void FixtureTearDown()
    {
        KillDatabase();
    }

    protected string ConnectionString
    {
        get
        {
            return String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True", 
                    TestServerName, TestDatabaseName);
        }
    }

    protected TContext Context { get; private set; }

    protected string TestDatabaseOriginalName { get { return "Database"; } }

    protected string ProjectName { get { return "CoolProject"; } }

    protected void RecreateContext()
    {
        Context = (TContext) Activator.CreateInstance(typeof(TContext), ConnectionString);
    }

    private string FolderForTempDatabase
    {
        get { 	return String.Format(@"R:\{0}.DatabaseTests\", ProjectName); }
    }

    private string TestDatabaseName
    {
        get { 	return FolderForTempDatabase + ProjectName + ".Tests"; }
    }

    private string TestDatabaseOriginalFileName
    {
        get {	return Path.Combine(TestDatabaseDirectory, TestDatabaseOriginalName + ".mdf"); }
    }

    private string TestDatabaseFileName
    {
        get { 	return Path.Combine(TestDatabaseDirectory, TestDatabaseName + ".mdf"); }
    }

    private void CreateFolderForTempDatabase()
    {
        var directory = new DirectoryInfo(FolderForTempDatabase);
        if(!directory.Exists)
        {
            directory.Create();
        }
    }

    private void RestoreDatabaseFromOriginal()
    {
        KillDatabase();
        CopyFiles();
        AttachDatabase();
    }

    private void KillDatabase()
    {
        Server server = Server;
        SqlConnection.ClearAllPools();
        if(server.Databases.Contains(TestDatabaseName))
        {
            server.KillDatabase(TestDatabaseName);
        }
    }

    private void CopyFiles()
    {
        new FileInfo(TestDatabaseOriginalFileName).CopyTo(TestDatabaseFileName, true);
        string logFileName = GetLogFileName(TestDatabaseFileName);
        new FileInfo(GetLogFileName(TestDatabaseOriginalFileName)).CopyTo(logFileName, true);
        new FileInfo(TestDatabaseFileName).Attributes = FileAttributes.Normal;
        new FileInfo(logFileName).Attributes = FileAttributes.Normal;
    }

    private void AttachDatabase()
    {
        Server server = Server;
        if(!server.Databases.Contains(TestDatabaseName))
        {
            server.AttachDatabase(TestDatabaseName, new StringCollection {TestDatabaseFileName, GetLogFileName(TestDatabaseFileName)});
        }			
    }

    private static string GetLogFileName(string databaseFileName)
    {
        return new Regex(".mdf$", RegexOptions.IgnoreCase).Replace(databaseFileName, "_log.ldf");
    }

    private static Server Server { get 	{ return new Server(TestServerName); } }

    private static string TestServerName { get { return "."; } 	}

    private static string TestDatabaseDirectory
    {
        get
        {
            var debugDirectory = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory);
            DirectoryInfo binDirectory = debugDirectory.Parent;
            DirectoryInfo testProjectDirectory;
            if(binDirectory == null || (testProjectDirectory = binDirectory.Parent) == null)
            {
                throw new Exception("");
            }
            return Path.Combine(testProjectDirectory.FullName, "Database");
        }
    }	
}


После использования которого тесты на взаимодействие с БД будут выглядеть примерно так:

[TestFixture]
public class ObjectFinderTest : DatabaseUnitTest<DatabaseDataContext>
{
    [Test]
    public void Test_NullWhenObjectNotExists()
    {
        //arrange
        var fakeIdentifier = 0;
        var finder = new ObjectFinder(fakeIdentifier, ConnectionString);

        //act
        var foundObject = finder.Find();

        //assert
        Assert.That(foundObject, Is.Null);
    }

    [Test]
    public void Test_SuccessfullyFound()
    {
        //arrange
        var insertedObject = ObjectsFactory.Create();
        Context.Objects.InsertOnSubmit(insertedObject);
        Context.SubmitChanges();

        var finder = new ObjectFinder(insertedObject.Id, ConnectionString);

        //act
        var foundObject = finder.Find();

        //assert
        Assert.That(foundObject.Id, Is.EqualTo(insertedObject.Id));
        Assert.That(foundObject.Property, Is.EqualTo(insertedObject.Property));
    }
}


Вуаля! Мы получили возможность тестирования слоя доступа к БД.

Случай 2. ASP.NET MVC WebAPI

При тестировании WebAPI один из вопросов заключается в том, каким образом построить модульные тесты так, чтобы можно было протестировать вызов нужных методов нужных контроллеров с нужными аргументами при отправке запроса на определенный url. Если предположить, что ответственность контроллера заключается только в том, чтобы перенаправить вызов соответствующему классу или компоненту системы, то ответ на вопрос о тестировании контроллера сведется к тому, чтобы перед запуском тестов динамически построить некое окружение, в котором контроллеру можно было бы отправлять нужные HTTP запросы и, используя mock'и, проверить правильность настроенного роутинга. При этом совершенно не хочется использовать для разворачивания тестового окружения IIS. В идеале, тестовое окружение должно создаваться перед запуском каждого теста. Это поможет модульным тестам быть достаточно изолированными друг от друга. С IIS в этом плане было бы достаточно непросто.

К счастью, с выходом .NET Framework 4.5 появилась возможность решить задачу тестирования роутинга достаточно просто. Например, используя следующие классы (в качестве DI контейнера используется Unity):

Код
public abstract class AbstractControllerTest<TController> where TController : ApiController
{
    private HttpServer _server;
    private HttpClient _client;
    private UnityContainer _unityContainer;

    [SetUp]
    public void BeforeTestExecuting()
    {
        _unityContainer = new UnityContainer();

        var configuration = new HttpConfiguration();

        WebApiConfig.Register(configuration, new IoCContainer(_unityContainer));

        _server = new HttpServer(configuration);
        _client = new HttpClient(_server);

        Register<TController>();
        RegisterConstructorDependenciesAndInjectionProperties(typeof(TController));
    }

    [TearDown]
    public void AfterTestExecuted()
    {
        _client.Dispose();
        _server.Dispose();
        _unityContainer.Dispose();
    }

    protected TestHttpRequest CreateRequest(string url)
    {
        return new TestHttpRequest(_client, url);
    }

    protected void Register<T>(T instance)
    {
        Register(typeof(T), instance);
    }

    private void Register(Type type, object instance)
    {
        _unityContainer.RegisterInstance(type, instance);
    }

    private void Register<T>()
    {
        _unityContainer.RegisterType<T>();
    }

    private void RegisterConstructorDependenciesAndInjectionProperties(Type controllerType)
    {
        var constructors = controllerType.GetConstructors();
        var constructorParameters = constructors
            .Select(constructor => constructor.GetParameters())
            .SelectMany(constructorParameters => constructorParameters);
        foreach (var constructorParameter in constructorParameters)
        {
            RegisterMockType(constructorParameter.ParameterType);
        }

        var injectionProperties = controllerType.GetProperties()
                    .Where(info => info.GetCustomAttributes(typeof(DependencyAttribute), false)
                    .Any());
        foreach (var property in injectionProperties)
        {
            RegisterMockType(property.PropertyType);
        }
    }

    private void RegisterMockType(Type parameterType)
    {
        dynamic mock = Activator.CreateInstance(typeof(Mock<>).MakeGenericType(parameterType), new object[] { MockBehavior.Default });
        Register(parameterType, mock.Object);
    }
}

public sealed class TestHttpRequest
{
    private readonly HttpClient _client;
    private readonly Uri _uri;

    public TestHttpRequest(HttpClient client, string url)
    {
        _client = client;
        _uri = new Uri(new Uri("http://can.be.anything/"), url);
    }

    public void AddHeader(string header, object value)
    {
        _client.DefaultRequestHeaders.Add(header, value.ToString());
    }

    public HttpResponseMessage Get()
    {
        return _client.GetAsync(_uri).Result;
    }

    public HttpResponseMessage Post(byte[] content)
    {
        return _client.PostAsync(_uri, new ByteArrayContent(content)).Result;
    }

    public HttpResponseMessage Put(byte[] content)
    {
        return _client.PutAsync(_uri, new ByteArrayContent(content)).Result;
    }

    public HttpResponseMessage Head()
    {
        var message = new HttpRequestMessage(HttpMethod.Head, _uri);
        return _client.SendAsync(message).Result;
    }
}


Теперь можно использовать эти классы для тестирования выдуманного контроллера, который возвращает сериализованные даты и объекты некоего класса Platform.

[TestFixture]
public class MyControllerTest : AbstractControllerTest<MyController>
{
    private MockRepository _mocks;

    protected override void OnSetup()
    {
        _mocks = new MockRepository(MockBehavior.Strict);
    }

    [Test]
    public void Test_GetDates()
    {
        //arrange
        var january = new DateTime(2013, 1, 1);
        var february = new DateTime(2013, 2, 1);

        var repositoryMock = _mocks.Create<IRepository>();
        repositoryMock
            .Setup(r => r.GetDates())
            .Returns(new[] {january, february});
        Register(repositoryMock.Object);

        //act
        var dates = ExecuteGetRequest<DateTime[]>("/api/build-dates");

        //assert
        _mocks.VerifyAll();

        Assert.That(dates, Is.EquivalentTo(new[] { january, february }));
    }

    [Test]
    public void Test_GetPlatforms()
    {
        //arrange
        var platform1 = new Platform {Id=1, Name = "1"};
        var platform2 = new Platform {Id=2, Name = "2"};

        var repositoryMock = _mocks.Create<IRepository>();
        repositoryMock
            .Setup(r => r.GetPlatforms())
            .Returns(new[] { platform1, platform2 });
        Register(repositoryMock.Object);

        //act
        var platforms = ExecuteGetRequest<Platform[]>("/api/platforms");

        //assert
        _mocks.VerifyAll();

        Assert.That(platforms, Is.EquivalentTo(new[] { platform1, platform2 }));
    }

    private T ExecuteGetRequest<T>(string uri)
    {
        var request = CreateRequest(url);
        var response = request.Get();
        T result;
        response.TryGetContentValue(out result);
        return result;
    }
}


Вот, собственно и все. Наши контроллеры покрыты модульными тестами.

Случай 3. Все остальное

А со всем остальным достаточно просто. Примеры модульных тестов на классы, которые содержат чистую логику, без взаимодействия с каким-либо внешним окружением, практически не отличаются от тех, которые предлагаются в популярной литературе типа «TDD by Example» Кента Бека. Поэтому каких-то особых хитростей здесь нет.

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

  • Упрощение архитектуры приложения. Основное правило здесь формулируется следующим образом: «Реализуется только то, что действительно нужно». Если в тесте описаны все сценарии и больше не удаётся придумать, как сломать логику, то нужно остановиться, привести код в более-менее приличный вид (выполнить рефакторинг) и со спокойной совестью лечь спать.
  • Документирование кода. Что может быть лучше, чем компилируемые и выполняемые примеры использования? Хорошо написанный тест является отличной документацией, которая, в отличие от комментариев, не потеряет своей актуальности. Если, конечно, будет контролироваться успешное прохождение тестов при изменении реализации логики программы.
  • «Подушка безопасности». Это, на мой взгляд, самое важное преимущество, которое можно получить от использования TDD в проекте. Тесты будут являться гарантией того, что программист, незнакомый с кодом, при внесении изменений сразу сможет увидеть, нарушили ли его изменения работу программы. Актуальные и полные модульные тесты дают отличную обратную связь. Кстати, в контексте «подушки безопасности» можно ответить на вопрос о целесообразности написания модульных тестов на, казалось бы, очевидный код. В случае командной разработки то, что очевидно одному разработчику может быть совершенно неочевидно другому. По разным причинам. В том числе и из-за различий в профессиональном уровне разработчиков (мы ведь помним про команду, верно?). И может сложиться ситуация, когда этому другому придётся вносить изменения в неочевидный или просто незнакомый для него код. И в этом случае модульные тесты могу уберечь систему от нарушения работоспособности и дать возможность разработчикам выполнять свои задачи более уверенно и эффективно.

Стоит, наверное, отметить, что перечисленные «плюшки» всегда будут свежими и вкусными при соблюдении принципа «test first». Изменились требования? Добавляем тест, изменяем код. Исправляем ошибку? Добавляем тест, изменяем код. Самое сложное — изменить восприятие тестов. Зачастую модульные тесты воспринимаются как нечто постороннее, чуждое «основному» коду. В этом и заключается, на мой взгляд, основное препятствие перед использованием TDD в полном объеме. И его нужно преодолеть, осознать, что модульные тесты и запрограммированный функционал — это части одного целого.

На сегодняшний день в проекте, над которым работает наша команда, около 1000 модульных тестов. Время сборки и запуска всех тестов на TeamCity составляет чуть больше 4 минут. Описанные в статье подходы позволяют нам тестировать практически все слои системы, контролируя изменение и развитие кода. Надеюсь, что наш опыт окажется для кого-нибудь полезным.
Поделиться публикацией

Похожие публикации

Комментарии 56

    +8
    Абсолютно согласен с тем, что нету ничего плохого в тестах с реальной сетью, файловой системой и базой данных. Проблема в том, что многие справедливо заметят, что это уже Интеграционные а не Модульные тесты. Не вижу ничего плохого в том, чтобы иметь разные типы тестов ( вплоть до силениумных ) и пользоваться ими всеми. Вопрос стоит во времени исполнения. Я считаю, что именно Модульные тесты должны запускаться в процессе сборки ( а не отдельно).

    Хороший совет про RamFS. Когда я налаживал тесты с реальной БД на PgSQL, то разница во времени исполнения достигала порядков при различных типах ФС ( ext3/ext4 ) и различных типах физических дисков.

    Кстати на моем проекте у нас есть слой, который скрывает построение запросов для БД и мы очень эффективно смогли решить проблему тестирования, написав бэкенд для SQLITE, который может работает только в оперативной памяти.
      0
      мы очень эффективно смогли решить проблему тестирования, написав бэкенд для SQLITE, который может работает только в оперативной памяти

      Почему Вам не подошли In-Memory Databases или размещение файла БД в /dev/shm/?
        0
        Почему не подошли — подошли. Я об этом и написал
          0
          Я Вас неправильно понял.
      +5
      >>На самом же деле нет ничего страшного, если в модульных тестах будет использоваться база данных или файловая система
      Нет. Это неверное утверждение. Т.к. ошибка будет «размазана» Вам придется дольше принимать решение в чем проблема: в базе данных или в тестируемом функционале?
      Если Вы другим тестом протестировали работу с базой данной, то ей уже можно доверять! Если же вдруг в работе кода с базой данных есть бага, то ее обнаружат Ваши тестировщики и тогда Вы напишите еще один юнит-тест.

      Задача разработчика: за вменяемое время быстро понять суть возникшей проблемы и именно поэтому он пишет юнит-тесты.

      Грубо говоря ситуция как с электроникой. Наладчик ставит замеряет осцилогрофом\мультиметром разные компоненты, чтобы быстрее отсечь заведомо исправные, в которых копаться не нужно. Когда Вы пишите юнит-тесты Вы разбиваете Вашу систему на куски, чтобы впоследствии быстрее принять решение откуда начать дебажить. При этом не надо вдаваться в крайность проверять абсолютно каждую функцию, а спросить себя «Оправдано ли будет потратить сейчас время на тест?». Применение мозга не карается законом!!!
        +1
        Эм. Строго говоря, наличие теста ускоряет написание кода. Вот как вы узнаете, что только что написанная функция действительно работает? Запустите всю систему с тем чтобы до неё добралось управление? Намного проще заранее написать 5 строк кода, верифицирующих результаты работы, а потом тыкать мышкой в точку слева от объявления функции-теста и жать «Run unit test». Вы получаете моментальный отклик на любое изменение, а так же сможете чётко для себя сформулировать, чего от функции хотите. Попробуйте, TDD это очень удобно.
          0
          >>Попробуйте, TDD это очень удобно.
          Ежедневно пробую! Ежедневно помогает. Набил очень много шишек и поэтому говорю: «Не любая функция должна быть покрыта юнит-тестом».

          >>Намного проще заранее написать 5 строк кода, верифицирующих результаты работы
          Это в теории. На практике это не только 1-2 позитивных случая, но также 4-6 негативных, чтобы быть уверенным что-таки ДА я могу доверять этому куску кода! А пишем мы далеко не 10 и даже не 20 ф-ций. А при этом рефакторим не мало. Если при рождении кода «на каждый чих» писать по тесту Вы придете к мысли «что-то млин это дело Волокитно». Это было. Это пройдено и именно из-за этой мысли многие бросали применять TDD вообще!

          Банальные примеры:

          I. когда не надо:

          1)
          bool ElfParser::isCorrected( const char * data, int data_len );
          Это проверка из примитивнейшего условия и на это писать тест глупо!

          2)
          bool SuperPuperClass::hasAnyStudents() { return !this.studentsList.empty(); }
          Что мы в этом примере должны проверять? Что метод std::list::empty() работает как надо?

          II когда надо:

          Функция prepareInternalFormatData() в которой, к примеру, достаточно много раз вызываются read(), seek(), tellg(). Это уже очень сложный функционал и в случае проблем в этой функции дебажить надо будет не мало! Это надо покрывать юнит-тестом.

          Всегда нужно руководствоваться здравым смыслом. Вместо мысли: «Раз функция значит надо тест», лучше применять вопросы: «Насколько сложна функция? Код функции уже устоялся?». Если оба вопроса «да», значит пишем тест, иначе мы просто задолбаемся актуализировать тесты.
            +4
            bool SuperPuperClass::hasAnyStudents() { return !this.studentsList.empty(); }
            Что мы в этом примере должны проверять? Что метод std::list::empty() работает как надо?

            Нет, здесь должны проверять, что hasAnyStudents() работает корректно.
              +1
              Я думаю, что Вы правы. Ведь наличие списка studentsList — это детали реализации. Реализация завтра может стать другой.
                0
                Вот когда будет, тогда и надо задуматься о том что пора писать тест. Это очень похоже на разработку «в прок». А вдруг еще понадобится функция ф1() ?! Ну давай-тогда напишем! А если еще понадобится ф2() ?! Ну давай и ее напишем. В конечном итоге приложение обрастает многими нафиг никогда пригодившимися вещами из-за которых сложность проекта возросла значительно!

                Есть такое правило, его удачно сформулировал Мартин в книге «Чистый код», гласит так: «Мы позволяем себе ошибиться один раз». Другими словами действие делается только тогда, когда в нем на практике возникла потребность, а не в умозрительном «А вдруг понадобится?».

                С тестами ровно также, детали могут изменяться и этот факт, но могут остаться в том же виде в каком их написали только что! Вот когда Вы ее изменили второй раз, тогда имеет смысл париться, в противном случае рискуете излишне усложнить проект. Кто-то конечно может сказать, что юнит-тесты писать не сложно. Ошибается! Проходит очень много времени, пока программист осознает, что он уже хорошо пишет юнит-тесты. Очень часто стоит только сказать коллеге: «Там упало пару тестов и надо...» он даже договорить не даст, сразу же в лоб «Извини, но мне надо пофиксить багу по задаче....». А задача фиксания тестов очень даже часто возникает, к примеру коллега набажил, а сам в отпуск ушел и кто-то должен его подменить ;)
                  0
                  Очень часто стоит только сказать коллеге: «Там упало пару тестов и надо...» он даже договорить не даст

                  У нас в команде всеми принято правило — мы не коммитим код, если не проходят модульные тесты. То есть, стараемся исключить ситуацию нестабильной ветки разработки. Поэтому таких проблем пока еще не возникало.

                  Вот когда будет, тогда и надо задуматься о том что пора писать тест.

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

                  Вполне возможно, что такой подход — специфика вашей работы.
                    0
                    >> мы не коммитим код, если не проходят модульные тесты
                    А если весь день писали код, завтра выходной — суббота или воскресение, или вообще отпуск? А вдруг не дай бог грипп? Будете откладывать комммит? Это глупо. Комитить надо, но не в trunk а в спец.ветку, а возможное фиксание тестов после комита или Вы или Ваши коллеги, это уже по ситуации.

                    >>Может получиться ситуация, когда тест будет написать очень сложно.
                    Значит и пользоваться будет сложно этим кодом! Значит надо рефакторить!

                      0
                      А если весь день писали код, завтра выходной — суббота или воскресение, или вообще отпуск? А вдруг не дай бог грипп? Будете откладывать комммит?

                      Чтобы избежать подобной ситуации используются маленькие коммиты. Ситуация, когда разработчик весь день работает над кодом и делает коммит только под конец рабочего дня крайне нежелательна. Потом и code review делать сложно и есть риск, что результаты работы за день не попадут в систему контроля версий.
                        0
                        Коммитить в пятницу вечером — дурной тон.
                      0
                      Что будет? Куда будет? Какая ф1()?

                      Вы ввели новый интерфейс — вы должны сейчас написать к нему тест. Вне зависимости от того, насколько проста его реализация.

                      У вас есть студенты, которые сейчас хранятся в studentList. И есть множество подсистем, которые написаны разными людьми и используют этот studentList.

                      Потом васяпупкен внёс коррективы в схему хранения, запустил тесты и увидел где что упало.
                      А то что теперь не работает hasAnyStudents() не увидел, так как вы к ней тест не написали, а он о её существовании может ничего и не знать.
                  0
                  bool ElfParser::isCorrected( const char * data, int data_len );

                  Извините, но не могу понять для чего нужна функция. Возможно, что при наличии теста мне было бы немного проще. Это я о том, нужно ли писать тест на очевидные вещи. Проблема в том, что они могут быть неочевидны кому-то ещё.

                  лучше применять вопросы: «Насколько сложна функция? Код функции уже устоялся?». Если оба вопроса «да», значит пишем тест, иначе мы просто задолбаемся актуализировать тесты.

                  Гм… верно ли я понял, что вы пишете тест после реализации и определения сложности функции?

                    0
                    >>Извините, но не могу понять для чего нужна функция.
                    Всего знать не получится! Ну никак! Я вот к примеру понятия не имею из чего делаются пломбы, которые используют стоматологи. Я не знаю всех нюансов TCP протокола. И что с того? Проще надо быть!
                    От погружения в тему предметной области для чего пишется код юнит-тесты не избавят! Когда программер «в теме» ему многое очевидно! Тогда достаточно взглянуть на прототипы и все становится понятным.

                    >>Извините, но не могу понять для чего нужна функция.
                    >>что они могут быть неочевидны кому-то ещё
                    Вопрос многих вещей в каком-либо проект связан со степенью осведомленности. Когда программер работает в проекте он может быть:
                    * новичок — и тогда лучше ему не юнит-тесты читать, а с проектом знакомиться к примеру doxygen
                    * автор проекта — его уж точно названиями не удивишь, он сам их придумывает как правило
                    * давно присоединившийся к проекту — он уже осведомлен и понимает где и что находится.

                    Теперь посмотрим на прототип метода:
                    Часть №1: Это парсер, это парсер ELF — некая хрень, возможно Executable Linux Format
                    Часть №2: isCorrected — на русский «это корректный? Этому набору байт можно хоть как-то верить?»

                    Все вместе «Определение корректности набора байт при ELF парсинге»

                    В чем проблема?

                    >>Гм… верно ли я понял, что вы пишете тест после реализации и определения сложности функции?
                    Сначала пишу пользовательский код, как будто предстоящий код уже написан. Исходя из написанного понимаю что и как нужно делать. Далее оцениваю какие куски кода очень мудренные, рефакторю сразу же и потом оценив сложность тех или иных функций пишу юнит-тесты
                      +3
                      isCorrected на русский можно перевести как «скоректирован ли», но никак не «корректен ли», тогда было бы «isCorrect». Кто и когда должен был что-то корректировать мне не ясно, возможно, автор кода просто плохо знает английскую языку.
                        0
                        >>английскую языку.
                        У нас у русских не то что с английским, даже с русским бывают проблемы и далеко не у глупых людей! ;)
                        0
                        и тогда лучше ему не юнит-тесты читать, а с проектом знакомиться

                        Я считаю, что знакомиться с проектом можно и посредством чтения модульных тестов. По крайней мере, мы в команде стремимся к такому состоянию тестов.

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

                        Я могу ошибаться, но мне такой подход напоминает readme driven development. Тоже очень интересный подход к разработке.
                          0
                          >>Я считаю, что знакомиться с проектом можно и посредством чтения модульных тестов
                          Да, согласен, можно и так. Но если честно я читаю чьи-либо тесты только в самую последнюю очередь! ;) А в своих домашних проектах, вообще аудио-запись используя, открываю IDE и нужный кусок кода, а потом на мобилу говорю место кода, ревизию и че вообще тут происходит. Потому что на будущее пояснить надо, а писать в лом ;)
                      0
                      Есть мнение (лень искать ссылку на статью), что даже геттеры/сеттеры свойств надо покрывать тестами. Обосновывают написание тестов на простые функции тем, что в дальнейшем их код может стать менее примитивным и перестать выполнять то, что от него ранее ожидалось.
                  +6
                  Unit-тест должен тестировать только класс, к которому применим, всё остальное должно быть заменено моками. Вариант с подключением кучи настоящих зависимостей — это уже не unit-тест.
                    0
                    Это не то чтобы настоящие зависимости. Это, своего рода, «моки базы данных». Или веб сервера.
                    +1
                    Мне кажется или статья сама себе противоречит?
                    Сначало говорится, что нужно поощерять разработчика скоростью выполнения тестов, да бы он меньше курил чаще запускал эти самые тесты. А потом говорится, что мол работаем с реальной бд, с реальной сетью, etc.

                    Соглашусь с первой частью, что модульные тесты нужно запускать чаще. А вот все реальная работа с физическими ресурсами это уже интеграционные или приемочные тесты, которые не требуют большой частоты запусков, хотя так же важны и способсвуют быстрейшему нахождению проблем в коде.
                      +1
                      Закономерный комментарий. Потребность тестировать взаимодействие с базой данных появилась из желания максимально использовать возможности модульного тестирования на проекте. То есть тестировать всё, что получится протестировать. Ну, а желание это продиктовано стоимостью ошибки. Чем раньше и полнее протестирована система, тем «дешевле», в конечном итоге, будут обнаруженные ошибки. Ведь даже если ошибки обнаружены отделом тестирования, их исправление является достаточно затратным занятием, не говоря уже об исправлении ошибок, обнаруженных в системе, отданной в эксплуатацию. Поэтому приходится искать компромисс между скоростью выполнения тестов и затратами на использование внешних ресурсов.

                      В общем, здесь скорее не противоречие, а некая «золотая середина».
                        0
                        Ну так ведь это другой вид, тестирования и в юнит тестировании их не должно быть, как выше сказал унит тесты должны тестировать только одну ответсвенность, в правильном коде в котором соблюдены правила SOLID, одной ответсвенности соответсвует один класс. В итоге все другое не как не отностится к модульным тестам, в соответсвии с чем запускается намного реже.
                          0
                          Ну так ведь это другой вид, тестирования и в юнит тестировании их не должно быть

                          Здесь опять вопрос в определении того, что считать unit тестами. В моём понимании это тесты на сущности, описанные в коде. На классы. Класс может использовать базу данных, а может не использовать. В зависимости от этого мы принимаем решение, какой будет модульный тест.

                          унит тесты должны тестировать только одну ответсвенность, в правильном коде в котором соблюдены правила SOLID, одной ответсвенности соответсвует один класс.

                          А если ответственность класса заключается в том, чтобы выбрать определённую сущность из хранилища? Некий ObjectFinder. В статье рассматривается пример, когда в качестве хранилища используется MS SQL. Поэтому и рассматривается вариант использования своеобразного mock'а БД, который максимально приближен к «оригиналу». То есть, если mock реального класса должен соответствовать интерфейсу оригинала, то mock реальной базы данных должен соответствовать структуре реальной базы данных. Изменения в структуре тестовой базы данных, необходимые для успешного прохождения модульного теста, в дальнейшем транслируются на реальную базу данных. То есть, здесь также может работать правило test first.

                          В итоге все другое не как не отностится к модульным тестам, в соответсвии с чем запускается намного реже.

                          Поэтому мы (в команде) решили немного изменить классификацию и попробовать запускать тесты намного чаще. Ту классификацию, которую мы себе взяли в работу, я описал в одном из комментариев ниже.
                            0
                            Класс может использовать базу данных, а может не использовать
                            Если класс содержит бизнес-логику, то он не должен стучаться к базе напрямую ни при каких обстоятельствах. Есть паттерн «репозиторий». Такие классы скрывают собой базу данных, предоставляя над ней некую абстракцию. На них тоже нужны юнит-тесты, но они тоже по-хорошему не должны стучаться к настоящей БД, а использовать ту же SQLite с хранилищем в памяти, потому что это быстро и позволяет запускать тесты не настраивая никаких дополнительных сервисов типа сервера баз данных, достаточно только IDE и компилятора.
                              0
                              Возможно я вас неправильно понял, но я ни в коем случае не против разделения на слои и выделения абстракций для БД. В процитированной вами фразе я имел в виду, что класс может быть как репозиторием, так и калькулятором. И тесты на эти классы будут разными. В статье описан возможный подход к тестированию того слоя, который отвечает за взаимодействие с базой данных и http запросами. Один из подходов. Причём приближенный к тем условиям, в которых будет работать код. С оглядкой на скорость выполнения тестов. При этом я совершенно согласен, что слой доступа к данным должен быть настолько «тонким» насколько это возможно. Но этот слой тоже должен тестироваться. И очень хорошо, что это можно сделать посредством модульных тестов.

                              использовать ту же SQLite с хранилищем в памяти

                              К своему стыду, я не знаю, как работает SQLite и насколько специфика работы с ней отличается от специфики работы с MS SQL. Вполне возможно, что классы, отвечающие за взаимодействие с БД, будут одинаково работать и с SQLite и с MS SQL. Просто изменяя connection string. Если это не так, то получается ситуация в которой мы тестируем не то, что будет работать на самом деле. Наверное, это даже хуже, чем отсутствие тестов. Так как даёт иллюзию работоспособности кода.
                              0
                              Предполагается, что внешние зависимости должны быть изолированы и тривиальны. На примере вывода на консоль
                              class ConsoleWriter
                              {
                              public virtual void WriteToConsole(string output){
                              Console.Write(output);
                              }
                              }
                              Экземпляр этого класса подается в конструктор, либо в свойства, и тесты уже проверяют, что тестируемый объект вызывает метод, а не пишет непосредственно в консоль. Сам же ConsoleWriter не тестируется в силу его тривиальности.
                              Так же и с базой данных. Не нужно имитировать базу данных. Нужно ее изолировать. С O/RM это делается вполне себе красиво.
                                0
                                Сам же ConsoleWriter не тестируется в силу его тривиальности.

                                Ну хорошо. А как определять границы тривиальности? В моём представлении, тривиальный код — это код, который просто делегирует вызов. Допустим, если нам нужен какой-нибудь статический метод. Например, пока не используем Microsoft Fakes, но тестировать вызов DateTime.Now уже нужно. В этом случае мы создаём «обёртку» с методом Now() которая просто делегирует вызов стандартному DateTime. И эту «обёртку» мы не тестируем.

                                Так же и с базой данных. Не нужно имитировать базу данных. Нужно ее изолировать. С O/RM это делается вполне себе красиво.

                                К сожалению, с базой данных не так. И ORM не со всеми запросами справляется так эффективно, как этого хотелось бы.
                                  0
                                  К сожалению, с базой данных не так. И ORM не со всеми запросами справляется так эффективно, как этого хотелось бы.

                                  Ну так это уже совсем другая история, которая зовется «НАГРУЗОНОЕ ТЕСТИРОВАНИЕ», при которых выявляется как раз тоги, слабые места приложения.

                                  PS. Извините не хотел не кого обидеть, не кого тролить.
                                    0
                                    Извините не хотел не кого обидеть, не кого тролить.

                                    Да вроде все разумные люди :)

                                    Ну так это уже совсем другая история, которая зовется «НАГРУЗОНОЕ ТЕСТИРОВАНИЕ», при которых выявляется как раз тоги, слабые места приложения.

                                    Подразумевается ведь, что нагрузочное тестирование проводится на корректно работающей системе? А как понять, что система работает корректно? Нагрузочные тесты вроде не очень для этого подходят.

                                    Допустим, есть метод некоего класса, который предназначен для выборки данных из БД — DataSelector. Выборки данных по определённым условиям. Понятно, что в модульных тестах на сущности, использующие этот класс, будет использоваться mock нашего DataSelector'а. Но ведь хочется протестировать и сам DataSelector. Не соврёт ли, возвращая данные из БД? Поэтому пишем модульные тесты на сам DataSelector. Потом, проведя нагрузочное тестирование, о котором вы говорите, мы понимаем, что вот этот DataSelector работает медленно. Ну вот использовали в качестве реализации метода какой-нибудь ORM для скорости разработки, а он строит неоптимальные запросы. Решили в целях оптимизации переписать метод без использования ORM. Чистый ADO.NET. Или Dapper какой-нибудь. И мы можем это сделать. Потому что у нас уже есть код, проверяющий как должна работать выборка этого DataSelector'а. То есть, я пытаюсь донести мысль, что нельзя просто сказать, что мы здесь используем ORM, поэтому тело метода, использующего ORM, считаем тривиальным и не тестируем.

                                    Ну, а нагрузочное тестирование имеет смысл проводить после того, как стало понятно, что система прошла все проверки и работает корректно. Не смог найти кому принадлежит фраза: «Медленно, но правильно работающая программа лучше, чем работающая быстро, но неправильно». Может быть фразу переврал, но надеюсь, что суть передал верно.
                                      0
                                      На самом деле не хочу показатся упертым ослом :), соглашусь с вашей точкой зрение, ну с одой оговоркой. Что в большенстве случаев не стоит тестировать так. Допустим тот же DataSelector, по умолчанию должен работать как нам нужно(его разработчик — по идеи написал тесты и описал как правильно работать с ним в документации). а мы не верим разработчики и пишем не нужные тесты на него, и боимся то что обыного мока будет не достаточно.

                                        0
                                        его разработчик — по идеи написал тесты и описал как правильно работать с ним в документации

                                        Подождите, подождите. Скорее всего мы не поняли друг друга. Я, когда говорю про тестирование сущностей работающих с базой данных, имею в виду те сущности, которые мы реализуем сами. То есть, наш код. Реализующий нашу бизнес логику.

                                        Если код внешний по отношению к нам, то он (этот код) модульными тестами не покрывается.
                                          0
                                          Короче я потерял, ход ваших мыслей, в предыдущем коменте вроде как говорите про ORM, и уровень DAL, а в этом уже про бизнес логику. и про то что не стоит тестить уже написаное, тот же самый орм.
                                            +1
                                            Хорошо. Попробую проиллюстрировать на выдуманном примере, который только описывает общую идею.

                                            Допустим, у нас есть следующий класс, который так или иначе взаимодействует с базой данных. Класс реализует определённый интерфейс.

                                            public class ArticlesRepository : IArticlesRepository
                                            {
                                                // Здесь используется ORM. Неважно какая.
                                                private ORM _orm = new ORM();
                                            
                                                // Пример вымышленный. Вместо массива может быть всё, что угодно.
                                                public Article[] GetPublishedArticles()
                                                {
                                                    return _orm.Articles.Where(a => a.Published).ToArray();
                                                }
                                            }
                                            


                                            И есть класс, который использует наш выдуманный репозиторий. Например, такой:

                                            public class ArticlesClient
                                            {
                                                private IArticlesRepository _articlesRepository;
                                            
                                                public ArticlesClient(IArticlesRepository articlesRepository)
                                                {
                                                    _articlesRepository = articlesRepository;
                                                }
                                            
                                                public int CalculatePublishedArticles()
                                                {
                                                    return _articlesRepository.GetPublishedArticles().Length;
                                                }
                                            }
                                            


                                            Этот ArticlesClient тоже может реализовывать какой-нибудь интерфейс. И использоваться в любом необходимом месте. В контроллере или ещё где-нибудь.

                                            Модульные тесты на ArticlesClient не будут использовать базу данных. В качестве реализации IArticlesRepository будет использоваться mock.

                                            Модульные тесты на ArticlesRepository будут использовать базу данных, разворачиваемую в памяти. В статье описан способ с использованием MS SQL.

                                            ORM же — внешний код по отношению к нашему. Возможно, это просто библиотека или набор библиотек, которые пришли к нам из NuGet. Мы не тестируем код этой библиотеки. Мы тестируем то, что с помощью него делаем. Нашу логику.

                                            Как-то так.
                                              0
                                              Все теперь понятно, стало что вы имели в виду.

                                              Как по мне то такие вещи не обязательно тестировать именнов в модульных тестах.
                                              Почему бы не тестированать работу ArticlesRepository в интреграционных тестах? которые запускаются не так часто как модульные тесты?
                                                0
                                                Желание использовать модульные тесты для классов, подобных ArticlesRepository вызвано тем, что хочется как можно раньше видеть ошибки реализации. Соответственно, как можно быстрее их исправлять.

                                                Тем более, что слой доступа к данным, на мой взгляд, является одним из важных компонентов системы.
                                        0
                                        Как уже сказано, это не юнит тесты. Это можно назвать интеграционными или behavior тестами. И они пишутся, как правило после реализации. Юнит тесты в рамках TDD пишутся до реализации и не должны тестировать внешние зависимости в принципе.
                                        Тест не должен звучать как «объект должен достать из базы, потом посчитать что-то», он должен звучать как «объект должен вызвать метод, получающий что-то и посчитать».

                                        Я в последнее время склоняюсь к мысли, что тесты в рамках TDD нужны не для того, чтобы чего-то там защитить, чтобы кто-то потом не сломал, а для того, чтобы код был простой и чистый. Простой код просто тестировать, его проще потом поддерживать. Соответственно код который трудно тестировать — трудно поддерживать. TDD заставляет разработчика писать хороший код.
                                          0
                                          Как уже сказано, это не юнит тесты. Это можно назвать интеграционными или behavior тестами. И они пишутся, как правило после реализации. Юнит тесты в рамках TDD пишутся до реализации

                                          Вопрос терминологии опять же. Если под «этим» подразумеваются тесты на сущности, работающие с БД, то в описываемом подходе вполне возможно писать тесты до реализации. Мы, собственно, на проекте так и делаем.

                                          Тест не должен звучать как «объект должен достать из базы, потом посчитать что-то», он должен звучать как «объект должен вызвать метод, получающий что-то и посчитать».

                                          Тесты на объект который должен вызвать метод и выполняющий подсчёт выглядят именно так. Другое дело, что на метод, который получает что-то мы тоже пишем тесты. Разворачивая БД в памяти.

                                          Я в последнее время склоняюсь к мысли, что тесты в рамках TDD нужны не для того, чтобы чего-то там защитить

                                          Я бы сказал «не только для того». Это одно из преимуществ TDD. Дополнительно к тем, которые вы описали.
                                            0
                                            Кстати, насчет терминологии, год назад я ненадолго попал в проект, где юнит тестами называли экселевский файл. Я сначала был в ступоре, как они смогли их туда впихнуть, потом оказалось, что это тест план.
                          +2
                          Использование настоящей базы данных, даже если и на рамдиске, создает дополнительную нагрузку на чистку базы данных перед каждым независимым тестом, иначе может случиться так, что один тест, записавший в таблицу какие-нибудь данные и не удаливший их, отвалит другой тест, в совершенно другом месте, причем без отладки обнаружить причину отвала — невозможно. А отлаживать модульные тесты — это то же самое, что забивать гвозди в молоток — когда инструмент создает нам дополнительную работу. Модульные тесты — это замена отладки. То есть, по не прошедшему тесту, должно быть очевидно, какую строчку в коде надо править, чтобы тест прошел. Оставим реальные ресурсы интеграционным тестам: модульный тест пусть работает со стабами.
                          Чаще всего необходимость тестировать классы, подсовывая им реальные ресурсы, приходится из двойной ответственности оных. То есть они и принимают какое-нибудь интересующее нас бизнес решение и лезут в базу или в сеть. Между тем, если сеть или базу спрятать за своей абстракцией, то:

                          1. Бизнес логика становится более повторно используемой с другими базами
                          2. Тесты становятся более выразительными и лаконичными
                          3. Происходит эпическое разделение ответственности, что сказывается на архитектуре приложения исключительно положительно

                          Лично я люблю модульное тестирование за не указанный вами пункт: модульное тестирование является экзаменом на дизайн. Если тест лаконичен, значит класс легко повторно использовать и у него простая ответственность. Если мне легко тестировать, значит будущим клиентам будет легко использовать. В том числе и повторно!
                            +1
                            Лично я тоже склоняюсь к использованию реальной базы данных для тестирования работы тех компонентов, которым требуются данные из БД. Особенно в небольших проектах. Практически всегда можно обойтись Sql Compact'ом 4ой версии и инициализировать отдельную БД при каждом запуске теста, примерно вот так:

                            public TestContext TestContext { get; set; }
                            
                            [TestInitialize]
                            public void SetupTestConnectionString()
                            {
                                string dbFilename = Path.Combine(TestContext.DeploymentDirectory, GetType().Name + ".sdf");
                                Database.DefaultConnectionFactory = new System.Data.Entity.Infrastructure
                                    .SqlCeConnectionFactory("System.Data.SqlServerCe.4.0", Path.GetDirectoryName(dbFilename), 
                                    string.Format("Data Source={0}", dbFilename));
                                TestContext.AddResultFile(dbFilename);
                            }


                            Тест обрабатывается за 1-2 секунды. Правда используется MSTests, вместо NUnit.
                              +1
                              То, о чём вы говорите верно в том случае, если используется одна база данных на весь набор тестов. В этом случае тесты становятся связанными между собой через базу данных и приходится чистить базу после выполнения каждого теста. И вполне возможна ситуация, при которой изменения в одном тесте влекут за собой падение всего набора. Тогда да, приходится отлаживать тесты. Не самая приятная ситуация. У нас был такой опыт. После которого мы пришли к тому решению, которое и описано в статье. База данных всегда новая. Создается перед выполнением теста. Полностью чистая. И каждый тест наполняет базу именно теми данными, которые ему нужны для работы. То есть выполняются предусловия, действие и постусловия.

                              Про экзамен на дизайн — спасибо!
                                0
                                Ну, создание базы каждый раз с нуля можно больно ударить по производительности. Тем более, что при более сложных организациях объектов (когда они друг на друга ссылаются), ваше требование держать тест коротким сводится на нет, потому что стадия arrange растягивается на 10-20 строчек, если не больше. В этом случае возникает естественное убрать из тестов дубликацию arrange-а в SetUp, но тогда SetUp становится узким местом, так как заполняет всеми возможными данными для всех тестов, так что скоро тесты начинают все равно неявно влиять друг на друга через SetUp. Если проверяются простые операции, типа что работают инсерты и селекты — этого но произойдет. Но если начать проверку бизнес логики методом заполнения базы данных — берегитесь этой сложности.
                                Плюс в работе с реальной базой данных есть тот недостаток, что сложно, например, проверить пейджинг. Чтобы проверить, что при наличии 1000 записей вернутся только 10, эти 1000 записей приходится пихать в базу. А это приводит к замедлению выполнения теста.
                                  0
                                  Ну, создание базы каждый раз с нуля можно больно ударить по производительности.

                                  На сегодняшний день сборка проекта плюс запуск всех модульных тестов составляет чуть больше 4 минут. Это не то чтобы больно, но да, хочется быстрее. Про создание базы данных я неверно выразился. Под созданием базы данных подразумевается копирование файлов БД на RAM диск и подключение этих файлов к экземпляру SQL сервера. Опять же, это только один из возможных подходов к тестирования слоя взаимодействия с БД.

                                  Тем более, что при более сложных организациях объектов (когда они друг на друга ссылаются), ваше требование держать тест коротким сводится на нет, потому что стадия arrange растягивается на 10-20 строчек, если не больше.

                                  Подобная ситуация может также возникнуть при неправильном использовании паттерна «репозиторий». То есть, когда начинает появляться масса методов, которые логически друг с другом не связаны. Как правило, методы подобных классов связаны между собой только общим контектом подключения к БД. Это довольно легко проверить. Если при добавлении в сигнатуру метода контекста БД метод может стать статическим, то это оно и есть. Обычно подобные классы имеют тенденцию разрастаться до состояния «невлезания» в экран. Тесты на подобные классы очень сложны для реализации. Вернее, начинается всё вроде бы неплохо, но со временем код теста превращается в одну большую нечитаемую массу. В этой ситуации можно попробовать провести декомпозицию такого «репозитория». Превратить методы в классы. Для которых будут свои тесты.

                                  Но если начать проверку бизнес логики методом заполнения базы данных — берегитесь этой сложности.

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

                                  Плюс в работе с реальной базой данных есть тот недостаток, что сложно, например, проверить пейджинг. Чтобы проверить, что при наличии 1000 записей вернутся только 10, эти 1000 записей приходится пихать в базу.

                                  Для проверки постраничной выборки совсем необязательно вставлять в базу данных 1000 записей. Цель ведь заключается в том, чтобы проверить сам механизм постраничной выборки, а не тот факт, что из 1000 записей вернется только 10. Ведь можно 10 записей разбить на страницы? Нужно только указать размер страницы. Для теста он может быть равен 2 записям. Если со стороны заказчика есть требование, что на странице должно отображаться только 10 записей, то это требование может быть проверено при функциональном тестировании.
                                0
                                Для своих проектов весь слой базы данных я выделяю в отдельный интерфейс (или несколько).
                                Это сделано для того, чтобы можно было легко поменять реализацию БД, или вообще перейти на другой тип хранилища.

                                Получается, что этот слой содержит
                                1. Запросы к БД
                                2. Какую-то нехитрую логику.

                                К примеру, метод
                                User getOrCreateUser(String firstName, String lastName)
                                Ищет пользователя по имени, если не находит — создает пользователя и заполняет служебные колонки: dateCreated, dateUpdated.

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

                                Как бы вы все сделали?
                                И как называются такие тесты? Которые запускаются с юнит-тестами, на них похожи, полностью автономны от внешних зависимостей и т.п.
                                  +2
                                  Я бы во-первых, сделал две отдельные функции — найти пользователя и создать пользователя (вот и разделение ответственности в полной красе). В класс, в котором живут эти функции сделал бы protected доступ к DataSet-у, с которыми бы работали обе функции. Обе функции lookupUser и createUser (для которой отдал бы в класс интерфейс, скрывающий системное время, ибо время — это тоже внешний ресурс) сделал бы виртуальными. Далее, тестил этот класс самошунтированием — т.е. унаследовал бы мою тестовую фикстуру от него, проверил, что сначала при вызове функций lookupUser и createUser в моем DataSet-е все хорошо. Затем, замокав функции lookupUser и createUser проверил бы, что при возвращении нулевого количества пользователей из lookupUser фасадная User getOrCreateUser(String firstName, String lastName) вызывает createUser.
                                    0
                                    Подход ваш понял, классно.

                                    Один момент по поводу моей реализации тестов:
                                    По ходу подготовки к тесту и его запуска, автоматически тестируется еще:
                                    1. Слой миграций — все ли миграции БД прошли на in-memory БД и все ли хорошо. Тест неявно это покажет.
                                    2. Слой ORM. Бывает, что не очень понятно, как замаппить такую-то колонку БД на поле объекта, особенно в связях many-to-many и каскадных сохранениях данных. Тесты с участием реального ORM очень помогают не «удивляться» особенностям ORM, вылезшим на PROD.
                                      0
                                      Полезность ваших тестов бесспорна. Но я такие называю интеграционными. Так как они тестируют систему в своей целосности. А модульными тестами я называю те, что проверяют мелкие шажки и логику отдельного класса. Поэтому его и надо изолировать.
                                    +3
                                    И как называются такие тесты? Которые запускаются с юнит-тестами, на них похожи, полностью автономны от внешних зависимостей и т.п.

                                    Терминология и определения это вообще очень скользкая тема. Если жестко следовать всем определениям, то часть кода тестируется (как правило, не самая важная), а часть нет. В итоге, если сказать, что мы вот это и вот это не тестируем, то количество кода в нетестируемых местах начинает планомерно увеличиваться.

                                    В общем, мы решили использовать другую классификацию тестов.

                                    Функциональные тесты — это тесты, предназначенные для проверки функционирования какого-либо компонента программной системы. Грубо говоря, если наша система состоит из набора исполняемых модулей, то тестирование одного такого модуля будет являться функциональным. То есть, компонент системы является «чёрным ящиком» для которого описывается, что при входном значении X на выходе будет значение Y или, если рассматривать приложение типа WinForms, описание может выглядеть так: «Если нажать на эту кнопочку, то в этом поле появится такое-то значение». Проверка подобных условий и будет являться функциональным тестированием.

                                    Интеграционные тесты — это тесты, предназначенные для тестирования системы в целом. Не отдельных компонентов, как в случае с функциональными тестами, а всей системы. Тесты этой группы самые «дорогие». Ведь зачастую, чтобы развернуть систему, требуется достаточно много времени и ресурсов. Иногда даже приходится дёргать системных администраторов, которые не всегда горят желанием заниматься проблемами разработчиков. В общем, интеграционное тестирование я отношу к разряду «экстремальных».

                                    Ну и наконец, модульные или unit тесты — это тесты, предназначенные для проверки логики работы программных компонентов. Таких как классы, методы, процедуры или функции. Эти тесты самые «дешёвые» среди трёх. Для того, чтобы протестировать логику работы метода или функции не требуется прилагать какие-то значительные усилия. Всё, что нужно уже есть или доступно в рабочем окружении программиста. Ну, а раз модульные тесты наиболее доступны по сравнению с функциональными и интеграционными, то имеет смысл сосредоточиться именно на них и постараться получить от их использования максимум выгоды, что, собственно, и предполагает TDD.
                                  0
                                  К вопросу о юнит-тесты vs. интеграционные тесты, старенькое видео о плюсах и минусах:
                                  www.devclub.eu/2011/06/06/asolntsev-real-life-unit-tests/
                                    0
                                    Для запуска тестов взаимодействующих с БД, но без изменения структуры, можно использовать другой механизм — транзакции в DCOM. Он корректно работает с ADO, LINQ2SQL, Entity.

                                    В NUnit наследовать все тесты с БД от класса:
                                    namespace TestNamespace
                                    {
                                    	using System.EnterpriseServices;
                                    	using NUnit.Framework;
                                    
                                    	[Transaction(TransactionOption.Required)]
                                    	public class RepositoryTestsBase : ServicedComponent
                                    	{
                                    		[TearDown]
                                    		public virtual void Teardown()
                                    		{
                                    			System.Data.SqlClient.SqlConnection.ClearAllPools();
                                    
                                    			if (ContextUtil.IsInTransaction)
                                    			{
                                    				ContextUtil.SetAbort();
                                    			}
                                    		}
                                    	}
                                    }
                                    


                                    Есть некоторые особенности использования, но они очень легко обходятся.
                                      0
                                      1. В конторе, где я сейчас работаю, следующая схема:
                                      Два билда в TFS:
                                      — Один просто билд+юнит тесты, второй — билд+интеграционные тесты.
                                      — Второй в начале воссоздаёт виртуальную машину из чистого образа, инсталлирует в неё все msi и запускает систему. После этого запускает интеграционные тесты, работающие с реальной БД и прочая. Для совсем внешних зависимостей используется самодельный SoapMock.

                                      Архитектура разбита так, что в юнит тестах Entity не тестируется (её как бы тестирует микрософт), но есть ДБ слой, который эмулируется.

                                      2. Если в контроллерах много бизнес логики — это уже признак того, что возможно не всё чисто. Бизнес логику стоит выносить из контроллера, пусть он занимается только роутингом и сборкой моделей из данных, получаемых из отдельных модулей. Вот как раз эти модули можно легко и просто тестировать без привязки к контексту контроллера. И одтельно, если хочется, можно тогда тестировать сами контроллеры, передавая им эмулированые провайдеры бизнес логики. DI rulez.

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое