Разработка команды запроса данных из базы

  • Tutorial

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


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


describe('Request', () => {

    const Request = require('./Request');

    it('execute should return promise', () => {

        const request = new Request();

        request.execute().then((result) => {

            expect(result).toBeNull();
        });
    });
});

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


class Request {

    constructor(){}

    execute(){
        return new Promise((resolve, reject) => {
            resolve(null);
        });
    }
}
module.exports = Request;

Выполняю npm test и наблюдаю в консоли зеленую точку выполнившегося теста.


Итак. У меня есть запрос, и он умеет выполнятся. В реальности однако, мне надо будет каким-то образом информировать мой запрос о том, в какой таблице ему следует искать нужные данные и каким критериям эти данные должны соответствовать. Попробую написать новый тест:


it('should configure request', () => {

    const options = {
        tableName: 'users',
        query: {  id: 1 }
    };
    request.configure(options);

    expect(request.options).toEqual(options);
});

Нормально? На мой взгляд вполне. Поскольку у меня есть теперь уже два теста, в которых используется экземпляр переменной request, то инициализацию этой переменной я вынесу в специальный метод, выполняющийся перед запуском каждого теста. Таким образом в каждом тесте у меня будет свежий экземпляр объекта запроса:


let request = null;

beforeEach(() => {
    request = new Request();
});

Реализую этот функционал в классе запроса, добавлю в него метод, который сохраняет настройки в переменной экземпляра класса, как это и демонстрирует тест.


configure(options){
    this.options = options;
}

Запускаю выполнение тестов и вижу теперь уже две зеленых точки. Два моих теста успешно выполнились. Однако. Предполагается однако что запросы мои будут адресоваться базе данных. Сейчас уже пожалуй стоит попробовать посмотреть с какой стороны запрос будет получать информацию о базе данных. Вернусь к тестам и напишу какой-нибудь код:


const DbMock = require('./DbMock');
let db = null;

beforeEach(() => {
    db = new DbMock();
    request = new Request(db);
});

Мне кажется такой вот классический вариант инициализации через конструктор вполне удовлетворяет моим текущим требованиям.


Естественно я не собираюсь в модульных тестах использовать интерфейс к реальной базе данных MySQL, с которой работает наш проект. Почему? Потому что:


  1. Если вместо меня, кому то из моих коллег потребуется поработать над этой частью проекта, и выполнить модульные тесты то прежде, чем они смогут хоть что-нибудь сделать, им придется потратить силы и время на установку, и настройку собственного экземпляра сервера MySQL.
  2. Успешность выполнения модульных тестов, будет зависеть от правильности предварительного заполнения данными, используемой базы сервера MySQL.
  3. Время запуска тестов с использованием базы данных MySQL будет значительно более длительным.

Ладно. А почему бы к примеру не задействовать в модульных тестах какую-нибудь базу данных в памяти? Работать она будет быстро, а процесс ее настройки и инициализации можно автоматизировать. Все так, но в настоящий момент я не вижу никаких преимуществ от использования этого дополнительного инструмента. Мне кажется что мои текущие потребности быстрее и дешевле (не нужно тратить время на изучение) можно удовлетворить с помощью классов и методов заглушек и псевдообъектов, которые будут лишь имитировать поведение интерфесов, которые предполагается использовать в боевых условиях.


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


Что из этого следует? Из этого следует что я должен доработать код класса Request так, чтобы выполнение запроса соответствовало интерфейсам, экспортируемым моим боевым инструментам. А значит теперь код должен выглядеть вот так:


class Request {

    constructor(db){
        this.db = db;
    }

    configure(options){
        this.options = options;
    }

    execute(){
        const table =
            this.db.Model.extend({
                tableName: this.options.tableName
            });

        return table.where(this.options.query).fetch();
    }
}
module.exports = Request;

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


class DbMock {
    constructor(){}
}

module.exports = DbMock;

Запущу тесты еще раз. Что теперь? Теперь принцесса Jasmine сообщает мне что мой DbMock не реализует свойство Model. Попробую придумать что-нибудь:


class DbMock {

    constructor(){
        this.Model = {
            extend: () => {}
        };
    }
}

module.exports = DbMock;

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


const options = {
    tableName: 'users',
    query: { id: 1 }
};

it('execute should return promise', () => {

    request.configure(options);

    request.execute().then((result) => {

        expect(result).toBeNull();
    });
});

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


Как и предполагалось, метод extend, свойства Model, класса DbMock вернул нам undefined, в связи с этим естественно у нашего запроса нет никакой возможности вызвать метод where.


Я уже сейчас понимаю что свойство Model, класса DbMock — следует реализовать за пределами собственно класса DbMock. В первую очередь из-за того, что реализация заглушек необходимых чтобы имеющиеся тесты выполнились, потребует слишком большого количества вложенных областей видимости при инициализации свойства Model прямо в классе DbMock. Это будет совершенно невозможно читать и понимать… И это однако не остановит меня от такой попытки, потому что я хочу убедиться что у меня по-прежнему есть возможность написать всего несколько строчек кода и заставить тесты выполниться успешно.


Итак. Вдох, выдох, слабонервным покинуть помещение. Дополняю реализацию конструктора DbMock. Та-даааамммм....


class DbMock {

    constructor(){
        this.Model = {
            extend: () => {
                return {
                    where: () => {
                        return {
                            fetch: () => {
                                return new Promise((resolve, reject) => {
                                    resolve(null);
                                });
                            }
                        };
                    }
                };
            }
        };
    }
}

module.exports = DbMock;

Жесть! Однако твердой рукой запускаем тесты и убеждаемся что Jasmine снова показывает нам зеленые точки. А это значит мы все еще на правильном пути, хотя кое-что у нас непозволительно опухло.


Что дальше? Невооруженным глазом видно что свойство Model псевдо-базы данных должно быть реализовано как нечто совершенно отдельное. Хотя навскидку и не понятно как оно должно быть реализовано.


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


describe('Users', () => {

    const users = require('./Users');

    it('should contain one user', () => {

        expect(Array.isArray(users)).toBeTruthy();
        expect(users.length).toEqual(1);

        const user = users[0];

        expect(user.Id).toEqual(1);
        expect(user.Name).toEqual('Jack');
    });
});

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


const Users = [
    { Id: 1, Name: 'Jack' }
];

module.exports = Users;

Теперь тесты выполняются, а мне приходит в голову что семантически Model, в пакете bookshell это поставщик интерфейса доступа к содержимому таблицы в базе. Не зря же мы в метод extend передаем объект с именем таблицы. Почему он называется extend, а не к примеру get, я не знаю. Возможно это просто недостаток знаний об API bookshell.


Ну да бог с ним, ибо теперь у меня в голове есть идея на тему следующего теста:


describe('TableMock', () => {

    const container = require('./Users');
    const Table = require('./TableMock');
    const users = new Table(container);

    it('should return first item', () => {

        users.fetch({ Id: 1 }).then((item) => {

        expect(item.Id).toEqual(1);
        expect(item.Name).toEqual('Jack');
        });
    });
});

Поскольку в моменте мне нужна реализация, лишь имитирующая функциональность реального драйвера хранилища, то я классы я именую соответствующим образом, добавляя суффикс Mock:


class TableMock {

    constructor(container){
        this.container = container;
    }

    fetch() {
        return new Promise((resolve, reject) => {
            resolve(this.container[0]);
        });
    }
}

module.exports = TableMock;

Но fetch не единственный метод, который я предполагаю использовать в боевой версии, так что добавляю еще один тест:


it('where-fetch chain should return first item', () => {

    users.where({ Id: 1 }).fetch().then((item)=> {

        expect(item.Id).toEqual(1);
        expect(item.Name).toEqual('Jack');
    });
});

Запуск которого, как и положено отображает мне сообщение об ошибке. Так что дополняю реализацию TableMock, методом where:


where(){
return this;
}

Теперь тесты выполняются и можно переходить к размышлениям на тему реализации свойства Model в классе DbMock. Как я уже предполагал, это будет некий поставщик экземпляров объектов типа TableMock:


describe('TableMockMap', () => {

    const TableMock = require('./TableMock');
    const TableMockMap = require('./TableMockMap');
    const map = new TableMockMap();

    it('extend should return existent TableMock', () => {

        const users = map.extend({tableName: 'users'});

        expect(users instanceof TableMock).toBeTruthy();
    });
});

Почему TableMockMap, потому что семантически это оно и есть. Просто вместо имени метода get, используется имя метода extend.


Поскольку тест падает, делаем реализацию:


const Users = require('./Users');
const TableMock = require('./TableMock');

class TableMockMap extends Map{

    constructor(){
        super();
        this.set('users', Users);
    }

    extend(options){

        const container = this.get(options.tableName);

        return new TableMock(container);
    }
}

module.exports = TableMockMap;

Запускаем тесты и видим шесть зеленых точек в консоли. Жизнь прекрасна.


Как мне кажется прямо сейчас уже можно избавиться от страшной пирамиды инициализации в конструкторе класса DbMock, воспользовавшись замечательным TableMockMap. Не будем откладывать это, тем более что неплохо бы уже было бы выпить чаю. Новая реализация восхитительно изящна:


const TableMockMap = require('./TableMockMap');

class DbMock {

    constructor(){
        this.Model = new TableMockMap();
    }
}

module.exports = DbMock;

Запускаем тесты… и упс! Наш самый главный тест падает. Но это даже и хорошо, потому что это был тест-заглушка и теперь мы просто обязаны его исправить:


it('execute should return promise', () => {

    request.configure(options);

    request.execute().then((result) => {

        expect(result.Id).toEqual(1);
        expect(result.Name).toEqual('Jack');
    });
});

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

Поделиться публикацией

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

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

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