Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

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

Сразу оговорюсь, что примеры приводятся применительно к языку 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 минут. Описанные в статье подходы разрешают нам тестировать фактически все слои системы, контролируя метаморфоза и становление кода. Верю, что наш навык окажется для кого-нибудь пригодным.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB