воскресенье, 30 января 2011 г.

Прототипы для Smart Cards


О чем речь

Многие реальные проекты уже сегодня используют методологию TDD (Test-Driven Development). Вместе с тем количество интересующихся данным вопросом постоянно увеличивается, а это будет способствовать тому, что и в будущем количество таких проектов будет неуклонно расти. Причины этому достаточно очевидны – использование методологии способствует улучшению качества кода на самом раннем этапе проектов, снижая количество ошибок, в том числе и архитектурных, и существенно уменьшает затраты на поддержку проекта в дальнейшем.
Вместе с тем, использование TDD для новичков и для тех, кому написание юнит-тестов в диковинку,  сопряжено с некоторыми проблемами и вопросами, возникающими на этапе создания этих самых тестов.  К примеру, абсолютно очевидно, что некоторый код, который используется в проекте, не может быть покрыт тестами. Например, в случае, когда необходимо осуществлять непосредственную работу с  базами данных или обращаться к «железу». Здесь на помощь приходят средства прототипирования, иначе известные как mock-фреймворки. Именно о них я и хочу поговорить в своей статье и показать, как их можно использовать на примере объектов, работающего с устройствами, известными как Smart Cards, или, говоря проще, с идентификационными пластиковыми карточками и считывающими устройствами (кард-ридерами).
В повседневной жизни я имею дело с платформой .NET и использую ее в своих проектах, соответственно здесь и далее речь будет идти о средствах и подходах в первую очередь применительно к .NET.

Два слова о Mock

Mock-фреймворков для целевой платформы существует большое количество. К числу наиболее распространенных можно отнести TypeMock Isolator, Rhino Mock, NMock и Moq. У каждого из них есть свои достоинства и недостатки. Поскольку это вопрос отдельной статьи – я не буду на этом останавливаться сейчас. Для статьи я выбрал Moq (официальный сайт: http://code.google.com/p/moq/), как наиболее легковесный и интуитивный. Отмечу, что тот же TypeMock Isolator или же Rhine – продукты куда более мощные и
 содержат в себе гораздо больше возможностей. Однако для большинства случаев, как в частности и в рассматриваемом примере, функциональности Moq – больше чем достаточно.
Еще один момент, на который хотелось бы обратить внимание перед рассмотрением реального примера – это принцип работы mock-фреймворков. Как они помогают в написании тестов для кода, который невозможно оттестировать? Как известно, чудес не бывает, и если код должен обращаться к железу напрямую, то никакие фреймворки не избавят от этой необходимости. Однако в данном случае из компонента, который работает с железом можно выделить интерфейс, в котором будут определены методы подключения, отключения, чтения и записи данных устройства. Далее с помощью Mock-фреймворка на основании полученного интерфейса можно создать объект, который будет использоваться остальным кодом, и будет эмулировать работу с железом, замещая вызовы API методов своими переопределенными методами, которые в точности эмулируют логику работы с железом. В результате для всего остального кода можно беспрепятственно написать юнит-тесты. Таким образом, непокрытым останется только реализация объекта, которая обращается к реальному устройству. А так как основную часть кода такого объекта составляют вызовы API методов, то этим можно пренебречь – так как, по сути, подобная логика не может быть оттестирована без реального обращения к реальному устройству. 

Как быть со Smart Cards

Итак, перед нами стоит задача – создать прототипы для объектов, которые будут работать cо Smart Card и соответственно с кард-ридерами. Прототипы будут созданы с помощью mock-фреймворка Moq.
 Прежде всего, нужно определиться с тем, что мы будем делать со Smart Cards в нашем примере и соответственно иерархией объектов, с которой мы будем иметь дело. При использовании Smart Cards нам, очевидно, понадобиться, собственно, сама карточка и считывающее устройство (кард-ридер), которое необходимо подключить к компьютеру. Обращение к карточке из нашего кода может быть возможным только в том случае, если она находится в считывающем устройстве. После того как мы убедились что, карточка доступна мы должны иметь возможность:
·         Считать из нее идентификационную информацию (предположим, в качестве таковой будет служить серийный номер карточки, который прописывается при прошивке карточки)
·         Проверить на правильность PIN-код который введет пользователь (PIN-код так же прописывается при прошивке)
·         После того как введен правильный PIN-код, должны быть доступны операции записи некоторых данных (предполагается что операции чтения могут быть доступны и без PIN кода, так как иначе могут возникнуть сложности с чтением серийного номера), которые будут храниться на карточке и представляют для нас первоочередной интерес.
·         Если PIN-код не введен, или введен, но неправильный – доступ к этим данным должен быть запрещен.

Проектирование иерархии

Таким образом, собрав воедино все требования, которые предъявляются к нашим объектам, которые будут работать с карточками, можно остановиться на интерфейсе, похожем на этот:
    public interface ISmartCard
    {
        string ReadSerial();
        string ReadData();
        void WriteData(string data);
        bool CheckPinCode(string pin);
    }

Здесь кажется все просто и понятно – на каждую операцию с карточкой отдельный метод. Если вы обратили внимание, для упрощения, в качестве данных, которые будут храниться на карточке, я взял обыкновенную строку – в реальных случаях, очевидно, имело бы место использование отдельного класса для этих целей, но поскольку нам это не так важно –можно ограничиться для этих целей и простой строкой.
К сожалению только одного интерфейса, который рассмотрен выше, нам будет недостаточно. Помимо того, что мы уже имеем, в лице созданного интерфейса, нам необходимы операции по получению списка считывающих устройств в системе (наверняка их может оказаться несколько), определению, имеется ли в конкретном устройстве карточка и, наконец, еще один метод, который должен возвращать объект, реализующий предыдущий интерфейс и  инкапсулирующий в себе работу с карточкой.  Таким образом мы приходим к следующему интерфейсу:
    public interface ISmartCardContext
    {
        string[] GetReaders();
        bool IsCardInReader(string reader);
        ISmartCard Connect(string reader);
    }

Теперь дела обстоят лучше – все моменты учтены и эти два интерфейса, похоже, полностью покрывают функциональность, которая необходима нам. К слову, если немного отступить от вопроса протипирования и поговорить именно про реализацию кода работающего со Smart Cards можно обратиться к бесплатной библиотеке                 pcsc-sharp. Она представляет собой высокоуровневую обертку над API Microsoft PC/SC  (иначе извествном как WinSCard) и впринципе способна решать технические вопросы по взаимодейтсвию вашего кода и Smart Cards. Если вы посмотрите поближе на эту библиотеку, то несложно будет найти много общего между нашими интерфейсами и реализацией самой библиотеки. Ключевыми элементами библиотеки являются классы SmartCardContext и SmartCard. Так что приведение классов библиотеки к нашим интерфейсам вряд ли окажется сложной задачей.   

Реализация прототипов

С помощью Moq можно реализовать объекты, которые будут замещать реальные классы, работающие с физическими устройствами. Для этого, в первую очередь, нужно определиться какой серийный номер должен возвращать наш прототип, какой PIN-код должен восприниматься как корректный и какие данные должны изначально быть на карточке.
Для этого разумно завести константы, как показано ниже:
private const string SerialNumber = "123";
private const string CorrectPin = "1111";
private const string InitialData = "It's my data";

Дальше нам нужно создать сам экземпляр прототипа, как правило инициализация прототипа выглядит так:

var mock = new Mock<ISmartCard>();

Да данном этапе мы получили экземпляр прототипа. Сам по себе он не реализует наш интерфейс. В дальнейшем, что бы из этого прототипа получить реализацию ISmartCard необходимо воспользоваться свойством Object:

ISmartCard card = mock.Object

Но прежде чем получить реализацию нам нужно настроить прототип в соответствии с нашими требованиями. Для этого мы будем использовать следующие методы:
Setup – метод прототипа, который будет указывать, логику какого метода объекта мы настраиваем.
Retruns – метод прототипа, который будет задавать, что должен возвращать метод объекта
Callback – метод прототипа, который будет изменять внутреннее состояние объекта.
Таким образом, метод, возвращающий наш серийный номер можно настроить следующим образом:

mock.Setup(sc => sc.ReadSerial()).Returns(SerialNumber);

С методом, возвращающим данные – тоже ничего необычного – все также как и для метода возвращающего серийный номер:
mock.Setup(sc => sc.ReadData()).Returns(InitialData);
С методом, который проверяет правильность введенного PIN-кода, не все так просто. Во-первых, он должен возвращать положительный результат только в том случае, если на вход метода поступит CorrectPin и в этом случае, объект должен как-то запомнить, что правильный PIN код был введен. К счастью, в связи с тем, что мы испольтзуем Moq, реализация выглядит проще, чем это звучит на словах. Для хранения состояния о том, что был введен правильный PIN, мы выделим переменную и будем ее устанавливать в случае правильно введенного PIN:
var authenticated = false;

mock.Setup(sc => sc.CheckPinCode(CorrectPin)).Re urns(true).Callback(() => authenticated = true);

Эту же переменную мы будем проверять при записи данных. Кстати, для того что бы ReadData возвращал новое значение после того как мы запишем новые данные его нужно немного изменить, так введем дополнительную переменную и будем возвращать ее вместо того что бы просто возвращать InitialData:
var data = InitialData;
mock.Setup(sc => sc.ReadData()).Returns(() => data);
mock.Setup(sc => sc.WriteData(It.IsAny<string>())).Callback<string>((v) =>
                                                                {
                                                                    if (authenticated)
                                                                        data = v;
                                                                });
  
Вот и все, прототип для ISmartCard готов. Что бы пользоваться им было удобно – можно вынести его создание в отдельный метод:
private static ISmartCard GetSmartCard()
{
    var data = InitialData;
    var authenticated = false;
    var mock = new Mock();
    mock.Setup(sc => sc.CheckPinCode(CorrectPin)).Returns(true).Callback(() => authenticated = true);
    mock.Setup(sc => sc.ReadSerial()).Returns(SerialNumber);
    mock.Setup(sc => sc.ReadData()).Returns(() => data);
    mock.Setup(sc => sc.WriteData(It.IsAny<string>())).Callback<string>((v) =>
                                                         {
                                                            if (authenticated)
                                                               data = v;
                                                          });
     return mock.Object;
 }

 С прототипом для ISmartCardContext все обстоит также просто и интуитивно понятно. Рассуждая так же, как и для ISmartCard мы в результате придем к следующему методу:

private const string ReaderName = "MockReaderName";

public static ISmartCardContext GetContext()
{
     var mock = new Mock();
     mock.Setup(c => c.GetReaders()).Returns(new[] { ReaderName });

     mock.Setup(c => c.Connect(It.Is<string>(s => s == ReaderName))).           Returns(GetSmartCard());

     mock.Setup(c => c.IsCardInReader(It.Is<string>(s => s == ReaderName))). Returns(true);

     return mock.Object;
}

Заключение
Вот и все. Объекты, возвращаемые полученными методами можно использовать в юнит-тестах, замещая ими реальные объекты и не беспокоясь ни о чем.
В данной статье я рассмотрел только самые базовые моменты использования Moq. Но даже они позволили создать прототип для достаточно сложных объектов. Дальше все зависит от вас. Как говориться ученье – свет, а не ученье – тьма. Приятной вам разработки и тестирования!