Monday, September 29, 2008

Entity Framework + NAnt - трудности с включением файлов модели в сборку

Вдогонку своему предыдущему посту про миграцию на .NET 3.5 SP1 пишу еще один, может, кому полезно будет. Еще одна беда подкралась внезапно, со стороны компиляции метаданных контекста EF в библиотеку. Дело в том, что у нас есть автобилды, построенные на Cruise Control + NAnt. Как вы сами, наверно, уже догадались, если NAnt'у не сказать, чтобы он билдил csdl, msl и ssdl в сборку, он сам не догадается. В принципе, ничего особенного, даже поста отдельного не заслуживает, однако здесь есть одна засада.

Дело в том, что раньше процессом разбивания файла edmx на три файла метаданных занимался специальный exe-файл, который назывался EdmxDeploy.exe, который по умолчанию не включал файлы метаданных в сборку, это нужно было делать отдельно. Так мы и жили, кто хотел – работал с файлами на диске, остальные – с ресурсами. Теперь же сборкой занимается специальный таск MSBuild, который лежит глубоко внутри и по умолчанию билдит метаданные в сборку. Мы переписали все строки соединения на ресурсы и думали, что дешево отделались. Не получилось: упал серверный билд. Причем, причина падения понятна – в сборке просто нет ресурсов с метаданными, но вот как их туда добавить, если у нас лишь edmx-файл? Как я ни пытался настроить билд edmx-файла, чтобы он и в сборку данные клал, и три файла метаданных мне на диск генерировал – ничего не получалось. Старый EdmxDeploy.exe разработчики с диска уже снесли – он вроде как не нужен. С горя уже полез изучать, как извне запустить MSBuild и получить нужные мне файлы метаданных отдельно, чтобы потом через <resources> добавить их в сборочный процесс NAnt'а. Однако снова спас гугл. В процессе поиска утилит случайно наткнулся на утилиту EdmGen2, которая, в отличие от EdmGen как раз и занимается тем, что умеет делить и собирать edmx.

У этой замечательной тулы есть несколько параметров, которые позволяют генерировать csdl, msl и ssdl по edmx, наоборот, валидировать edmx-модель, а также генерировать edmx-модель по базе и потом код по этой модели. Ну, и на закуску, эта тула поставляется с исходниками, так что вы можете все посмотреть и исправить в случае необходимости сами.

Все, что остается в нашем случае – это лишь добавить код генерации нужных нам файлов в postbuild-скрипт и добавить сгенерированные файлы в nant-скрипт.

Postbuild-скрипт:

rem Post-build Event Command Line:
rem "$(ProjectDir)postbuild.cmd" "$(ProjectDir)"

@echo off
cd "%1"
%WINDIR%\system32\xcopy "%1Model\Model.edmx" "%1EntitySchema\" /s /r /y

cd "%1EntitySchema"
EdmGen2.exe /FromEdmx CCF.edmx

Кусок nant-скрипта:

<target name="build">
    <csc ...>
        ...
        <resources prefix="Model">
            <include name="EntitySchema/Model.csdl" />
            <include name="EntitySchema/Model.ssdl" />
            <include name="EntitySchema/Model.msl" />
        </resources>
    </csc>
</target>

Thursday, September 25, 2008

Введение в mock-объекты. Классификация

Еще один длинный, но, надеюсь, полезный пост :) Пригодится тем, кто хочет начать писать unit-тесты, уже пишет их и хочет увеличить свои познания, и даже тем, кто тесты не пишет, но хочет быть в курсе того, что же это за зверь такой страшный - unit-тестирование.

Как я уже писал в предыдущем посте об unit-тестировании, иногда, для того, чтобы протестировать какой-нибудь кусок кода (например, метод), нужно довольно сильно постараться. Причем, это еще не тот вид извращений, когда вы тестируете методы UI, проблемы могут начаться с тестирования бизнес-логики. Дело в том, что очень часто тестируемый метод может вызывать методы других классов, которые в данном случае тестировать не нужно. Unit-тест потому и называется модульным, что тестирует отдельные модули, а не их взаимодействие. Причем, чем меньше тестируемый модуль – тем лучше с точки зрения будущей поддержки тестов. Для тестирования взаимодействия используются интеграционные тесты, где вы уже тестируете скорее полные use cases, а не отдельную функциональность.

Однако наши классы очень часто используют другие классы в своей работе. Например, слой бизнес логики (Business Logic layer) часто работает с другими объектами бизнес логики или обращается к слою доступа к данным (Data Access layer). В трехслойной архитектуре веб-приложений это вообще постоянный процесс: Presentation layer обращается к Business Logic layer, тот, в свою очередь, к Data Access layer, а Data Access layer – к базе данных. Как же тестировать подобный код, если вызов одного метода влечет за собой цепочку вплоть до базы данных?

В таких случаях на помощь приходят так называемые mock-объекты, предназначенные для симуляции поведения реальных объектов во время тестирования. Вообще, понятие mock-объект достаточно широко: оно может, с одной стороны, обозначать любые тест-дублеры (Test Doubles) или конкретный вид этих дублеров – mock-объекты. Я постараюсь использовать этот термин исключительно во втором случае, чтобы никого не путать, и не запутаться самому :)

Понятие тест-дублеров введено неким Gerard Meszaros в своей книге «XUnit Test Patterns» и теперь с подачи небезызвестного Мартина Фаулера эта терминология набирает популярность. Джерард и Мартин делят все тест-дублеры на 4 группы:

  • Dummy – пустые объекты, которые передаются в вызываемые внутренние методы, но не используются. Предназначены лишь для заполнения параметров методов.
  • Fake – объекты, имеющие работающие реализации, но в таком виде, который делает их неподходящими для production-кода (например, In Memory Database).
  • Stub – объекты, которые предоставляют заранее заготовленные ответы на вызовы во время выполнения теста и обычно не отвечающие ни на какие другие вызовы, которые не требуются в тесте. Также могут запоминать какую-то дополнительную информацию о количестве вызовов, параметрах и возвращать их потом тесту для проверки.
  • Mock – объекты, которые заменяют реальный объект в условиях теста и позволяют проверять вызовы своих членов как часть системы или unit-теста. Содержат заранее запрограммированные ожидания вызовов, которые они ожидают получить. Применяются в основном для т.н. interaction (behavioral) testing.

Поначалу эта классификация выглядит очень непонятной. Но если вдуматься, то можно разобраться, в чем заключается отличие между теми и иными типами объектов. Предположим, что вам нужно протестировать метод Foo() класса TestFoo, который делает вызов другого метода Bar() класса TestBar. Предположим, что метод Bar() принимает какой-нибудь объект класса Bla в качестве параметра и потом ничего особого с ним не делает. В таком случае имеет смысл создать пустой объект Bla, передать его в класс TestFoo (сделать это можно при помощи широко применяемого паттерна Dependency Injection или каким-либо другим приемлемым способом), а затем уже Foo() при тестировании сам вызовет метод TestBar.Bar() с переданным пустым объектом. Это и есть иллюстрация использования dummy-объекта в unit-тестировании.

К сожалению, редко можно обойтись простыми dummy-объектами. Иногда метод Bar() выполняет какие-то действия с ним (допустим, Bar() сохраняет данные в базу или вызывает веб-сервис, а мы этого не хотим). В таких случаях наш объект класса TestBar должен быть уже не таким глупым. Мы должны научить его в ответ на запрос сохранения данных просто выполнить какой-то простой код (допустим, сохранение во внутреннюю коллекцию). В таких случаях можно выделить интерфейс ITestBar, который будет реализовывать класс TestBar и наш дополнительный класс FakeBar. При unit-тестировании мы просто будем создавать объект класса FakeBar и передавать его в класс с методом Foo() через интерфейс. Естественно, при этом класс Bar будет по-прежнему создаваться в реальном приложении, а FakeBar будет использован лишь в тестировании. Это иллюстрация fake-объекта.

Со stub- и mock-объектами все немного сложнее, хотя и здесь есть от чего отталкиваться. Stub-объекты (стабы) – это типичные заглушки. Они ничего полезного не делают и умеют лишь возвращать определенные данные в ответ на вызовы своих методов. В нашем примере стаб бы подменял класс TestBar и в ответ на вызов Bar() просто бы возвращал какие-то левые данные. При этом внутренняя реализация реального метода Bar() бы просто не вызывалась. Реализуется этот подход через интерфейс и создание дополнительного класса StubBar, либо просто через создание StubBar, который является унаследованным от TestBar. В принципе, реализация очень похожа на fake-объект с тем лишь исключением, что стаб ничего полезного, кроме постоянного возвращения каких-то константных данных не требует. Типичная заглушка. Стабам позволяется лишь сохранять у себя внутри какие-нибудь данные, удостоверяющие, что вызовы были произведены или содержащие копии переданных параметров, которые затем может проверить тест.

Mock-объект (мок), в свою очередь, является, грубо говоря, более умной реализацией заглушки, которая уже не просто возвращает предустановленные данные, но еще и записывает все вызовы, которые проходят через нее, чтобы вы могли дальше в unit-тесте проверить, что именно эти методы вот этих вот классов были вызваны тестируемым методом и именно в такой последовательности (хотя учет последовательности и строгость проверки, в принципе, настраиваемая вещь). То есть мы можем сделать мок MockFoo, который будет каким-то образом вызывать реальный метод Foo() класса TestFoo и затем смотреть, какие вызовы тот сделал. Или сделать мок MockBar и затем проверить, что при вызове метода Foo() реально произошел вызов метода Bar() с нужными нам параметрами. Не совсем понятно? :) Вам нужно знать еще кое-что о unit-тестировании, чтобы понять разницу.

Unit-тестирование условно делится на два подхода:

  • state-based testing, в котором мы тестируем состояние объекта после прохождения unit-теста
  • interaction (behavioral) testing, в котором мы тестируем взаимодействие между объектами, поведение тестируемого метода, последовательность вызовов методов и их параметры и т.д.

То есть в state-based testing нас интересует в основном, в какое состояние перешел объект после вызова тестируемого метода, или, что более часто встречается, что в реальности вернул наш метод и правилен ли этот результат. Подобные проверки проводятся при помощи вызова методов класса Assert различных unit-тест фреймворков: Assert.AreEqual(), Assert.That(), Assert.IsNull() и т.д.

В interaction testing нас интересует прежде всего не статическое состояние объекта, а те динамические вызовы методов, которые происходят у него внутри. То есть для нашего примера с классами TestFoo и TestBar мы будем проверять, что тестируемый метод Foo() действительно вызвал метод Bar() класса TestBar, а не то, что он при этом вернул и в какое состояние перешел. Как правило, в случае подобного тестирования программисты используют специальные mock-фреймворки (TypeMock.Net, EasyMock.Net, MoQ, Rhino Mocks, NMock2), которые содержат определенные конструкции для записи ожиданий и их последующей проверки через методы Verify(), VerifyAll(), VerifyAllExpectations() или других (в зависимости от конкретного фреймворка).

То есть во многом это отличие можно назвать аналогичным отличию state machine diagram и activity diagram в UML: описывают они, в принципе, одно и то же, но разными способами. Иногда удобнее один, иногда второй.

Фаулер вот называет эти два подхода классическим (classical) и мокистским (mockist, ну и слово выдумал) unit-тестированием и делит программистов на тех, кто предпочитают первый и кто предпочитает второй подходы. Я бы этого не делал. Мне кажется, что иногда просто удобнее проверить состояние объекта, а иногда – его взаимодействие с другими объектами. Поэтому эти два подхода прекрасно уживаются вместе, когда вы понимаете, о чем идет речь, и что именно вы хотите сейчас проверить. Так же, как уживаются в одном тесте моки и стабы.

Вот такие пирожки. Теперь несколько примеров, которые иллюстрируют различия между этими двумя подходами и использования стабов и моков. Я также покажу, зачем в реальности может понадобиться interaction-тестирование, чтобы вам было проще выбирать тот или иной вид в будущем. Примеры используют NUnit и Rhino Mocks, хотя на их месте с небольшим изменением синтаксиса может оказаться почти любая другая пара фреймворков.

Допустим, у нас есть несколько простых классов:

public class Order
{
    public string ProductName { get; private set; }
    public int Quantity { get; private set; }
    public bool IsFilled { get; private set; }

    public Order(string productName, int quantity)
    {
        ProductName = productName;
        Quantity = quantity;
    }

    public void Fill(IWarehouse warehouse)
    {
        if (warehouse.HasInventory(ProductName, Quantity))
        {
            warehouse.Remove(ProductName, Quantity);
            IsFilled = true;
        }
    }
}

public class Warehouse
{
    private DataAccess db;

    public Warehouse()
    {
        db = new DataAccess();
    }

    public virtual bool HasInventory(string productName, int quantity)
    {
        return db.HasInventory(productName, quantity);
    }

    public virtual void Remove(string productName, int quantity)
    {
        db.Remove(productName, quantity);
    }
}

Вот такой набор классов. Класс Order обращается к классу Warehouse, а тот обращается к базе данных. Предположим, что мы тестируем метод Fill() класса Order. Вот пример тестирования с использованием стаба для state-based тестирования:

[Test]
public void TestFillingOrderWithRhinoStub()
{
    Order order = new Order(Talisker, 50);
    var stubUserRepository = MockRepository.GenerateStub<Warehouse>();

    stubUserRepository.Stub(x => x.HasInventory(Talisker, 50)).Return(true);
    stubUserRepository.Stub(x => x.Remove(Talisker, 50));

    order.Fill(stubUserRepository);
    Assert.IsTrue(order.IsFilled);
}

Пара пояснений по коду. Сначала мы создаем объект типа Order, затем – стаб для класса Warehouse. После этого мы при помощи mock-фреймворка говорим, что при вызове метода HasInventory с определенными параметрами этот метод должен нам вернуть true. Аналогичным образом переопределяем поведение метода Remove (а то еще вызовет реальный и будет бяка). Далее идет вызов метода Fill() с переданным стабом, после чего проверяется, что свойство IsFilled установлено в true. Как видите, ничего сложного. Однако данный тест обладает некоторыми недостатками. Во-первых, непонятно, что делать, если в тестируемом объекте нет свойства, аналогичного IsFilled. Как проверять правильность выполнения кода? Во-вторых, непонятно, что случится, если программист удалит или закомментирует вызов следующей строчки в коде метода Fill():

warehouse.Remove(ProductName, Quantity);

IsFilled устанавливается в true, тест проходит, но код-то уже не работает!

Обе эти проблемы легко разрешаются, если мы воспользуемся interaction тестированием с использованием мока. Для этого напишем другой тест:

[Test]
public void TestFillingOrderWithRhino()
{
    Order order = new Order(Talisker, 50);
    var mockUserRepository = MockRepository.GenerateMock<Warehouse>();

    mockUserRepository.Expect(x => x.HasInventory(Talisker, 50)).Return(true);
    mockUserRepository.Expect(x => x.Remove(Talisker, 50));
    mockUserRepository.Replay();

    order.Fill(mockUserRepository);
    Assert.IsTrue(order.IsFilled);
    mockUserRepository.VerifyAllExpectations();
}

Начало теста аналогичное, затем идет создание мока Warehouse, после чего идет несколько вызовов метода Expect с теми же параметрами, что и в предыдущем тесте. При помощи этого метода мы говорим моку, что мы ожидаем вызова этих методов с такими параметрами и нам в ответ на их вызовы нужно вернуть такие-то значения. Затем идет вызов метода Replay(), который переводит мок из режима записи ожиданий в режим их проверки, то есть запуска тестового метода. Все моки имеют несколько режимов работы (Record, Replay, Verify), это распространенный подход. Далее непосредственно запуск, проверка IsFilled и вызов нового для нас метода VerifyAllExpectations(). Последний как раз и делает всю работу по проверке вызовов методов, параметров и т.д. Теперь, если метод Remove оказался закомментированным, тест не пройдет. Кроме того, нам уже не так важна проверка состояния объекта Order. Если бы свойства IsFilled не было, ничего бы не изменилось, а так мы лишь проверяем, что оно было установлено в соответствии с алгоритмом. Теперь немного поэкспериментируем с кодом. Что, если мы уберем второй Expect или поменяем их местами? Есть несколько режимов строгости проверки, которые задаются через конструктор класса Mock, который также можно использовать для создания мока. В Rhino Mocks есть три уровня строгости: Loose, Strict и Default (Loose). В Loose-режиме мок проверяет лишь то, что все ожидаемые методы были вызваны из тестируемого метода, в то время как в Strict-режиме проверяется также, что не было любых других вызовов и что порядок вызову соответствует порядку ожиданий. В других фреймворках иногда есть и другие режимы. Таким образом, в нашем случае при изменении порядка тест бы прошел, но в Strict-режиме – уже нет. Еще один момент, который показывает отличие методов Expect от методов Stub (в моке они также доступны): методы, зарегистрированные в моке при помощи метода Stub невидимы для метода VerifyAllExpectations. То есть, если нужна проверка вызовов – используйте Expect. Также стоит отметить, что при помощи дополнительных методов типа Return вы можете не только указывать возвращаемые значения, но еще генерировать exception'ы (Throw), вызывать настоящий метод (CallOriginalMethod), задавать ограничения на параметры (Constraints), вызывать дополнительные методы (Callback, Do), работать со свойствами и событиями. В общем, список потрясающий.

Как видите, иногда удобно проверить состояние, иногда – вызовы и взаимодействие. Так что это не вопрос «или-или», это вопрос – «когда» :) Удачного вам unit-тестирования!

Monday, September 22, 2008

Unit-тестирование в аутсорсинге

Unit-тестирование в оффшорной аутсорсовой модели разработки ПО имеет достаточно интересную специфику. С одной стороны, почти все программисты и даже некоторые менеджеры знают, что написание unit-тестов – это полезная практика, которая помогает создавать более стабильный, качественный и модифицируемый код. С другой стороны лишь небольшое количество проектов имеют хотя бы какие-нибудь unit-тесты, а те проекты, на которых unit-тесты живут и развиваются – вообще единичные случаи. Сразу скажу, что я буду говорить в основном о компаниях города Харькова, т.к. знаю я по большинству лишь харьковский рынок, но мне кажется, что какие-то результаты анализа можно аппроксимировать на всю отрасль в целом.

Прежде всего, нужно определиться, зачем вообще нужно unit-тестирование:

  • Unit-тестирование дает уверенность, что код работает как нужно хотя бы при положительном тестировании, то есть, когда мы проверяем его лишь с точки зрения правильного результата.
  • Оно также позволяет проверить граничные и неправильные входные данные, которые могут попасть на вход определенному участку кода (отрицательное тестирование) и, соответственно, поставить дополнительную защиту в таких случаях (проверка NullReferenceException и прочее).
  • Наверно, самый основной пункт – более безопасное изменение функциональности и рефакторинг: unit-тесты можно сравнить с парашютом, который спасает программиста, если тот в своем желании что-то исправить зайдет слишком далеко и сломает другие части системы.
  • Unit-тесты защищают приложение от переоткрытия старых дефектов, когда уже были однажды исправлены. Для этого перед тем, как делать исправление, нужно написать unit-тест, который воспроизводит ошибку, потом исправить ее и запустить тест заново. Во-первых, это быстрая проверка исправления, во-вторых, постоянный запуск теста в будущем даст возможность быть уверенным, что ошибка не воспроизведется.
  • Unit-тесты также снижают время на тестирование и отладку приложения, т.к. очень много дефектов вылавливается автоматически самими тестами, а не QA.
  • В это сложно поверить, но unit-тесты поощряют отделение интерфейса от реализации, использование паттернов проектирования, уменьшают связность кода (coupling) и, в целом, улучшают код. Связано это прежде всего с тем, что для того, чтобы протестировать метод, часто нужно сделать дополнительные операции – создать заглушки или моки, передать их в нужный класс/метод. Иногда тестируемый метод настолько велик, что его стоит разделить на несколько более мелких и т.д.
  • Ну, и последнее: написание unit-тестов позволяет глубже понять тестируемый код и, соответственно, сделать его более качественным.

Все, что я перечислил, не является чем-то принципиально новым. Многие западные программисты уже давно взяли unit-тесты себе на вооружение. Концепция unit-тестирования и рефакторинга существует уже больше 10 лет. Количество unit test и mock фреймворков постоянно растет, создаются целые методологии и практики по использованию unit-тестов в разработке программного обеспечения (одна из практик XP, Test Driven Development, рефакторинг, CI и т.д.). Есть куча англоязычных форумов, блогов, статей, посвященных этому вопросу. В то же время, когда смотришь на разработку ПО в нашей стране, создается впечатление, что мы застряли в каменном веке :) Однако, такому положению дел есть достаточно простое объяснение: несмотря на то, что полезность unit-тестов трудно оспорить, выгодны они далеко не всегда.

Есть как минимум несколько отрицательных сторон unit-тестирования:

  • Написание unit-тестов занимает время. Согласно различным исследованиям, написание unit-тестов увеличивает процесс написания кода в среднем в 2 раза. Часто этого времени нет или это время дорого.
  • Unit-тесты требуют постоянной поддержки, что также увеличивает время разработки приложения. Здесь особых цифр нет, но, думаю, это увеличивает время еще процентов на 5-10.
  • Не весь код легко протестировать при помощи unit-тестов (например, UI). Создание поддержки таких тестов часто требует колоссальных трудозатрат.
  • Ну, и наконец, программисты ленивы, а написание тестов – это достаточно рутинная задача, поэтому программисты зачастую не любят/хотят писать unit-тесты.

Почему же тогда в некоторых случаях unit-тесты помогают в процессе разработки, а в некоторых – мешают ей? И самое главное: в каких случаях их стоит писать, а в каких – нет? Это была присказка, дальше идут во многом мои размышления, вам виднее, соглашаться с ними или нет.

Если проанализировать существующее положение дел, то можно увидеть, что unit-тесты в большинстве своем используются либо в компаниях-лидерах разработки ПО, где с охотой применяются различные нововведения в индустрии, либо в компаниях, которые разрабатывают свое ПО в течение многих лет. И это, в принципе, понятно. Несмотря на то, что unit-тестирование – уже не молодая техника, успешно и более-менее массово ее начали применять лишь недавно, с появлением гибких методологий и развития теоретической базы: появления концепции рефакторинга кода, подходов к рефакторингу и целых методологий написания кода на базе unit-тестирования, например, TDD. Рефакторинг без unit-тестов становится очень опасным занятием, поэтому они часто идут в паре. Кроме того, unit-тесты особенно полезны в тех случаях, когда код достаточно часто меняется, а не просто сидит сиднем, становясь со временем legacy-кодом, в который все боятся лезть. А вот это как раз больше подход к разработке больших приложений в течение многих лет, когда выходят новые версии, содержащие какие-нибудь усовершенствования и новые фичи. В таких приложениях качество – превыше всего и unit-тесты позволяют его достичь.

В то же время оффшорная аутсорсовая разработка – это зачастую разработка не таких долгостроев, а даже если это и долгострой, то клиента очень часто больше волнуют бюджет и сроки, и в меньшей степени – качество (а иначе, зачем он тогда отдал разработку в оффшор, а не сделал заказ у себя на родине, где и качество будет выше, и цена в разы больше). Такому клиенту будет сложно «продать» идею unit-тестирования, ведь часто бывает, что клиент даже обычное тестирование не хочет проводить, считая, что это могут сделать и программисты. И так сроки и бюджет поджимают, не до абстрактных преимуществ какой-то новомодной практики. Хотя, справедливости ради нужно сказать, что для многих типов приложений unit-тесты – это действительно избыточность. Если вы пишете небольшое приложение, в котором нет серьезных участков кода, требующих частых изменений, unit-тесты вам, возможно, и не нужны. Основная мощь unit-тестов – в их возможности быстро одернуть программиста, когда он внес в приложение код, разрушающий существующий функционал, а это очень часто ведет к тому, что дефект уходит в production, так как полное функциональное тестирование в конце жизненного цикла разработки на проектах бывает далеко не всегда.

На своем личном опыте я могу сказать, что из около 10 компаний города Харькова, о которых я что-то знаю, unit-тестирование хотя бы в каком-то виде есть лишь где-то на 15-20% проектов, а в полной мере оно используется и того реже.

Итак, попробуем понять, когда unit-тестирование полезно в аутсорсовой модели разработки и как лучше его использовать:

  • В динамически меняющемся приложении (например, если разработка идет по agile-методологии с постоянно меняющимися требованиями) unit-тесты могут, за счет увеличения времени разработки, существенно уменьшить время на отладку, тестирование и багфиксинг. В целом, затраты на последующую отладку зачастую компенсируют накладные расходы во время разработки.
  • В крупных или критичных к качеству приложениях unit-тесты могут существенно повысить качество результата и процесса в целом. Если у вас приложение такого типа, не раздумывайте долго – unit-тестирование вам необходимо.
  • Необходимо не забывать, что покрытие тестами 100% кода не защитит вас от ошибок, но лишь уменьшит их, поэтому 100%-ное покрытие не имеет особого смысла. В идеале unit-тестами должен быть покрыт лишь сложный и часто меняющийся код, в котором вероятность появления дефектов при изменении или рефакторинге выше, чем в остальном коде.
  • Для того чтобы уменьшить время на переделку тестов, лучше сразу же хорошо продумать архитектуру и дизайн приложения. Этот совет хорош не только с точки зрения unit-тестов, но и просто с точки зрения разработки ПО.
  • Для того чтобы уменьшить время на само написание тестов, лучше сразу стараться пользоваться различными паттернами проектирования, выделять интерфейсы, уменьшать связность кода. Опять же, совет касается не только и столько unit-тестирования, сколько разработки в целом.
  • Для избежания появления уже однажды исправленных дефектов следует писать unit-тесты на дефект перед тем, как исправлять его.
  • Для более систематического написания тестов можно применить какой-нибудь систематический подход к написанию unit-тестов, например, TDD. Это сложно и требует перестройки мозгов программистов, но практики TDD уверяют, что это дает отличные результаты в дальнейшем.
  • Внедрять unit-тесты, конечно же, проще на новом коде, на существующем же legacy-коде можно ввести следующее правило: не делать изменения в нем до тех пор, пока не будет написано несколько небольших тестов, проверяющих правильность его работы. Это даст возможность не только со временем покрыть тестами старый код, но еще и обезопасить себя от возможных последствий своих изменений.
  • Во многих случаях для написания unit-теста на метод необходимо серьезно извратиться, чтобы поставить заглушки на все внешние методы, которые вызывает тестируемый метод. Здесь можно посоветовать использовать любой из существующих mock фреймворков, например, Rhino Mocks, Moq или TypeMock.Net. Положа ногу на сердце, я бы рекомендовал Rhino Mocks, но иногда нужна мощь TypeMock.Net, или простота Moq – выбирайте, что вам больше по душе и по карману. Более подробно про моки и заглушки я расскажу в следующий раз.
  • Если вы понимаете, что на вашем проекте unit-тестирование нужно как воздух, а клиент не хочет и слушать об этом, вооружитесь аналитикой и опишите клиенту все «за» и «против» unit-тестов. Написание базовых тестов на основные классы и методы не займет много времени, но поможет вам сэкономить кучу времени в дальнейшем и, что более важно, поможет сделать продует более качественным и надежным.
  • Как я уже говорил, не все программисты любят писать тесты. Их тоже нужно расшевелить, как и клиента, показать достоинства, сделав обязанность писать тесты не каторгой, а приятной рутиной, которая позволяет уберечь себя в будущем от серьезных проблем.
  • Ну, и наконец, даже если вам сильно понравилась идея unit-тестирования, не забывайте, что, возможно, оно вам и не нужно. Действуйте прагматично и рационально. Не нужно бегать за всеми нововведениями в мире разработки ПО – это чревато.

А у вас на проекте используются unit-тесты? Какие практики вы используете, какие шишки набили?

Thursday, September 18, 2008

Entity Framework: миграция с beta 3 на SP1

В этом посте я бы хотел рассказать про наш опыт миграции приложения с Entity Framework beta 3 на релизную версию, которая вышла около месяца назад. Поэтому он будет прежде всего полезен таким же неудачникам пострадавшим, как и мы, хотя, думаю, содержит полезную информацию и для тех, кто только собирается использовать EF в своих проектах.

Несколько месяцев назад, в ответ на пост Сергея Розовика о EF beta 3 breaking changes я немного самонадеянно в комментариях написал, что у нас с этим особых проблем быть не должно. Так оно и вышло: по списку проблем и не было :) Однако, забегая наперед, скажу, что были проблемы в других местах, где мы их и не ожидали. Ну да где наша не пропадала :) Итак, по порядку.

Удаление старой версии EF

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

Для Entity Framework beta 3 нужно:

1. Удалить ADO.NET Entity Framework Tools из “Add or Remove Programs"

2. Удалить ADO.NET Entity Framework 1.0 (Pre-Release version) из “Add or Remove Programs"

Для Entity Framework SP1 pre-release (или beta):

1. Закачать себе Visual Studio 2008 Service Pack Preparation Tool

2. Запустить его и удалить VS2008 SP1 pre-release

Инсталляция VS 2008 SP1

Инсталляция – процесс очень простой, но в то же время долгий, т.к. инсталлятор построен по принципу «скачай себя сам за 3 часа». Скачать релизную версию можно здесь. Если вам нужен только .NET 3.5 SP1 (ну, для сервера, например), то можно воспользоваться отдельным инсталлятором. По опыту скажу, что проще всего запустить инсталлятор вечером перед уходом домой (перед этим снеся предыдущую версию, конечно же), и утром он вас порадует сообщением, что я уже типа скачался и даже установился, осталось только шмакнуть кнопочку Restart Now.

Непосредственно миграция

Здесь начинается самое интересное. Для начала всем желающим следует ознакомиться со списками breaking changes для beta 3 и pre-release. Прочитайте их внимательно и обращайтесь к ним в первую очередь в случае, если у вас будут проблемы с компиляцией вашего кода или неправильной его работой.

Задача миграции сводится к 3-м большим пунктам:

1. Создать новую версию модели EDMX. Этот пункт, наверно, менее актуален для ребят, которые начали работать с pre-release или уже успели перевести свои модели на него, но я бы все равно рекомендовал его, потому что править XML и потом думать” WTF is going on?” – не самое приятное занятие.

2. Поправить свой код, который обращается непосредственно к контексту EF или его внутренностям согласно изменениям в API (как раз они неплохо описаны в списках breaking changes)

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

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

1) Отставляем текущую версию файла EDMX в сторону или вообще исключаем из проекта, чтобы не путалась под ногами. Делаем Add -> New Item, выбираем ADO.NET Entity Data Model, потом Generate from database и дальше по накатанной, как обычно: выбираем таблицы, view, хранимые процедуры и т.д.

2) После того, как дизайнер услужливо построит модель, начинаем восстанавливать все наследования, удаляем ненужные ключи и переименовываем навигационные свойства, если нужно. Например, мне больше нравится видеть множественные имена (Orders, Users) для связей 1-to-* и *-to-*. Не забываем удалять ID из наследников и замапливаем их на ID родителя.

3) Заметил неприятный баг – для наследников все связи с другими сущностями почему-то теряются во время установки наследования. Поэтому восстанавливаем все маппинги навигационных свойств для наследников.

4) Добавляем хранимые процедуры в концептуальную модель. Важный шаг – не забудьте о нем.

5) Нажимаем validate для модели и вот здесь начинаются проблемы. Первая из них – это почему-то появляющийся для некоторых случаев <ReferentialConstraint>. Просто избавляемся от него через XML.

6) Следующими падают связи 1-to-1, которые прекрасно работали в beta 3. Возможно, теперь нужно что-то добавить для того, чтобы они заработали, или концепция просто поменялась – в любом случае пофиксили мы это через добавление primary key и изменение типа физичиской связи на 1-to-*, которое просто в концептуальной модели замапливается как 1-to-1. Немного криво, но работает.

7) Следующая проблема оказалась посерьезнее предыдущих и на нее вкупе было потрачено намного больше времени. Ни с того, ни с сего для сущностей, которые имеют несколько связей с какой-то другой сущностью или просто имеют связь саму на себя, начали выскакивать следующие сообщения об ошибках валидации:

"Error 3034: Problem in Mapping Fragments starting at lines 6250, 7344: Two entities with different keys are mapped to the same row. Ensure these two mapping fragments do not map two groups of entities with different keys to two overlapping groups of rows."

"Error 3034: Problem in Mapping Fragments starting at lines 2495, 2504: An entity from one EntitySet is mapped to a row that is also mapped to an entity from another EntitySet with possibly different key. Ensure these two mapping fragments do not map two unrelated EntitySets to two overlapping groups of rows."

Поколупавшись немного в схеме и погуглив, я-таки нашел причину и выход. Оказывается, теперь в таких случаях EF ожидает элемент <Condition> (выход нашелся в одной из веток форума MSDN, можно почитать еще и про вторую ошибку). Зачем это ему нужно, сказать сложно, но лечится это прямо так, как написано на форуме – добавлением этого элемента. Иногда возникает необходимость добавить этот же элемент и в парную связь. Например:

<AssociationSetMapping Name="FK_Entity1_UserCreated" TypeName="SomeNamespace.FK_Entity1_UserCreated" StoreEntitySet="Entity1">
  <EndProperty Name="User">
    <ScalarProperty Name="Id" ColumnName="CreatedBy" />
  </EndProperty>
  <EndProperty Name="Entity1">
    <ScalarProperty Name="Id" ColumnName="Id" />
  </EndProperty>
  <Condition ColumnName="CreatedBy" IsNull="false" />
</AssociationSetMapping>
<AssociationSetMapping Name="FK_Entity1_UserUpdated" TypeName="SomeNamespace.FK_Entity1_UserUpdated" StoreEntitySet="Entity1">
  <EndProperty Name="User">
    <ScalarProperty Name="Id" ColumnName="UpdatedBy" />
  </EndProperty>
  <EndProperty Name="Entity1">
    <ScalarProperty Name="Id" ColumnName="Id" />
  </EndProperty>
  <Condition ColumnName="UpdatedBy" IsNull="false" />
</AssociationSetMapping>

8) Исправляем все остальные возникшие ошибки валидации до тех пор, пока валидатор перестанет ругаться.

Далее начинаем изменять код. Нам пришлось пофиксить всего 4 места:

1) Заменить вызов GetEntityKey() на CreateEntityKey(). Замечу, что эта замена нигде в breaking changes не описана, пришлось действовать наощупь :)

2) Заменить свойство QueryTimeout на CommandTimeout (только для тех, кто мигрирует с beta 3)

3) Поменять connection strings для edmx-файлов. Если раньше для того, чтобы к ним добраться, приходилось извращаться различными способами, то теперь они автоматически билдятся в сборку и доступны из ресурсов через строку подключения наподобие этой:

metadata=res://*/Model.csdl|res://*/Model.ssdl|res://*/Model.msl;provider=System.Data.SqlClient;provider connection string=...

4) С некоторых пор KnownTypeAttribute стал автоматически добавляться в сгенерированные сущности, поэтому больше нет нужды добавлять его самим с помощью указания статического метода, возвращающего массив типов. Более того, теперь компилятор ругается на то, что у нас используется KnownTypeAttribute с указанием метода и другие его вариации для одного и того же класса, поэтому нужно просто удалить атрибут с методом.

5) Продолжаем в том же духе, пока у вас не останется ошибок компиляции

6) Ребилдим custom tool, если таковой был использован для кастомизации кодогенерации модели – очень полезная штука, рекомендую (примеры здесь и здесь). Мы через нее реализовали нормальный lazy load, который, впрочем и сыграл с нами злую шутку :) Подробности этой реализации я опущу до лучших времен, если кому интересно увидеть раньше – отпишите в комментах, я сделаю отдельный пост быстрее.

Итак, мы закончили с кодом. Те же, кто рискнул использовать сериализацию сущностей через DataContractSerializer в своих целях, читаем дальше :) Для меня до сих пор остается загадкой, почему навигационные свойства для EntityCollection и не-Reference навигационные свойства для EntityReference<T> оказались помеченными атрибутом DataMemberAttribute (то есть сериализуемые через DataContractSerializer), хотя они же помечены атрибутами SoapIgnoreAttribute и XmlIgnoreAttribute (то есть остальные сериализаторы их игнорируют), но видимо, так надо. Однозначно одно: в нашем случае сериализация этих свойств приводила к тому, что мы выкачивали ВСЮ базу данных (т.к. нас включен lazy loading на уровне кодогенерации) и пытались ее сериализовать, что приводило к неминуемому OutOfMemoryException. Мой вопрос на форумах MSDN остался без ответа, пришлось разбираться самому. Были разные варианты решения этой проблемы вплоть до того, чтобы отключать lazy load на время сериализации сущностей, но в результате пофиксили мы эту проблему просто и радикально – закомментировав ненужные DataMemberAttribute атрибуты через все ту же кастомную кодогенерацию при помощи гениальной строчки кода:

string formattedCode = code.Replace(
"[global::System.Xml.Serialization.SoapIgnoreAttribute()]\r\n        [global::System.Runtime.Serialization.DataMemberAttribute()]",
"[global::System.Xml.Serialization.SoapIgnoreAttribute()]\r\n        //[global::System.Runtime.Serialization.DataMemberAttribute()]");

Не правда ли, просто? Конечно, сама реализация достаточно тупая, но зачем придумывать что-то более гибкое, если и так работает на 100% эффективно. Вот когда выйдет EF v2 и в нем еще что-то поменяется в генерации – будем смотреть :)

Итого: миграция прошла успешно, осталось лишь установить SP1 на все девелоперские станции и сервера. Удачи и вам. Будут вопросы – пишите в комментарии, постараюсь помочь.

Thursday, September 4, 2008

Иркутск и панорамы из путешествия на Байкал

Наконец-то у меня дошли руки до панорам, сделанных в отпуске. Так что, кому еще не надоел поток фотографий с Байкала и Хамар-Дабана, милости просим на пикасу. Панорамы, в отличие от прошлого раза, я делал в Photoshop CS3. Как их там делать, можно почитать здесь или здесь. Я пользовался в основном авторежимом изменения позиции (в нем куски фотографии при склеивании лишь перемещаются, но сами никак не видоизменяются). Для байкальских панорам пришлось все руками настраивать, потому что в одном случае автомат не смог склеить панораму по центру, где было яркое солнце, а второй раз один из кусков был снят криво. В принципе, если помнить, что фотки делались с рук без штатива, то можно сказать, что снимки удались. Вот парочка панорам:


Закат на Байкале

Панорама озера Сердце

Там же я сегодня выложил фотографии из Иркутска, там тоже есть на что посмотреть. Вообще, Иркутск нам очень понравился. Центр чистенький, ухоженный, есть где погулять и посидеть. Ангара, набережные, красивые здания, много зелени. В центре Иркутска еще осталось очень много деревянных домов, выглядят они шикарно, многим уже наверно лет по 100. Город развивается ускоренными темпами, везде идет строительство. Цены приемлемые, хотя некоторые продукты дороже, чем в Харькове. Поразило количество праворульных машин, их здесь больше половины, все японские. Много таких моделей, которых в Европе просто нет. Чего стоит только Toyota Vista :) В Иркутске купить почти новую машину, пригнанную из Владика, стоит около 5000 долларов. Недорого, правда? Водителипо улицам носятся очень быстро, но в то же время, что удивило, стараются уважать пешеходные переходы, чего не встретишь в Харькове. Только пешеход зашел на переход – сразу останавливаются и пропускают. Или другой пример: даже ночью (!) наш таксист (!) на пустом перекрестке (!) остановился и подождал зеленый свет! Немыслимо...

PS. Да, и последнее, а вы знали, что вышла бета новой версии пикасы? :)

Monday, September 1, 2008

Аутсорсинг ПО = утечка мозгов

В этой нехитрой формуле выражается суть аутсорсинга программного обеспечения в нашей стране, да и не только в ней. В конце XX века в СССР, а потом и в странах СНГ стал популярен термин «утечка мозгов», который обозначает процесс потери определенной страной своих лучших (или просто хороших) специалистов в той или иной области, которые меняли свое место жительства или как минимум работы, таким образом принося пользу (и доход) другим странам.

Этот термин популярен и сейчас, причем можно говорить не только об утечке мозгов, но и утечке спортсменов, что в очередной раз явно продемонстрировали Олимпийские игры в Пекине. Однако разговор сейчас не о спортсменах. То, что многие хорошие специалисты Украины уезжают за границу жить и работать – ни для кого не секрет. Однако мало кто осознает, что утечка мозгов возможна и внутри страны. Каким образом? А как иначе, если не утечкой можно назвать то, что огромное количество IT-специалистов работают над проектами зарубежных заказчиков? Ведь, по сути, эти же люди могли бы применить свои умения и опыт на отечественных проектах, причем не обязательно на государственных.

У нас в стране ситуация с IT просто аховая. Взять, к примеру, Голландию (да и многие другие развитые страны ЕС), где по словам моих недавно вернувшихся оттуда друзей политика государства направлена на то, чтобы люди покупали товары по большей части онлайн, на это выделяются деньги. Это экономит средства на людской труд, да и просто автоматизирует то, что реально можно автоматизировать. Или взять Израиль, откуда вернулись другие мои друзья. Там автоматизировано все, что можно было и даже то, что на первый взгляд кажется невозможным, например, сельское хозяйство: графики полива угодий, дойка коров на фермах и подача корма и воды. Я уже не говорю про Японию (в которой никто из моих друзей пока не был :)), где практически все производство идет на автоматизированной основе, где идет массированное создание роботов и попытки создания полноценного ИИ. И ведь делается все это не просто так, а потому что это РЕАЛЬНО экономит людям деньги. МНОГО денег. А что у нас? Укрзализныця вот молодцы – сделали возможность смотреть билеты на поезда, но, насколько я видел на вокзале – у них по-прежнему ПО написано под DOS (привет, Turbo Vision). А если я хочу бронь? А если я хочу заплатить онлайн? Есть электронные магазины, в которых можно что-то купить, но ими пользуется абсолютное меньшинство населения. Это пока не популярно, и если компьютер есть дома уже у многих, то выход в интернет или локальную городскую сеть – у меньшинства, причем большая часть из этого меньшинства – это IT-специалисты, их вообще можно не считать. Но дело даже не в сети – есть множество других мест, где необходима автоматизация. Государственные и не только предприятия, промышленность, торговая сфера, почти вся сфера услуг, сфера обучения. Чего стоит только автоматизация коммунальных платежей населения. Это же сколько можно было бы сэкономить денег и, главное, человеческого времени и нервов. И ведь самое интересное – в стране есть достаточное количество специалистов, которым по плечу лет за 5-8 перевернуть всю страну с ног на голову. Была бы соответствующая политика и финансирование. А пока их нет, эти люди (то есть мы) поднимают экономику других стран.