Developing a team to request data from the database
- Tutorial
Currently engaged in the implementation of interaction with a supplier of KYC services. As usual, nothing cosmic. You just need to select from your database some rather large set of copies of various records, upload them to the service provider and ask the supplier of these records to check.
The initial stage of processing contains a dozen identical operations with sending requests for extracting data of a certain specific user from various tables of the database. There is an assumption that in this case a sufficiently large part of the code can be reused as an abstraction Request
. I will try to suggest how this could be used. I will write the first test:
describe('Request', () => {
const Request = require('./Request');
it('execute should return promise', () => {
const request = new Request();
request.execute().then((result) => {
expect(result).toBeNull();
});
});
});
Looks like pretty good? Perhaps imperfect, but at first glance it seems that Request
this is in essence команда
, which returns Promise
with the result? From this it is possible to begin. I jot down the code so that the test can be run.
classRequest{
constructor(){}
execute(){
returnnewPromise((resolve, reject) => {
resolve(null);
});
}
}
module.exports = Request;
I execute npm test
and observe in the console the green point of the test that has been completed.
So. I have a request, and he knows how to execute. In reality, however, I will need to somehow inform my query about which table he should look for the necessary data and what criteria this data must meet. I'll try to write a new test:
it('should configure request', () => {
const options = {
tableName: 'users',
query: { id: 1 }
};
request.configure(options);
expect(request.options).toEqual(options);
});
Fine? In my opinion completely. Since I now have two tests that use an instance of a variable request
, I will bring the initialization of this variable into a special method that runs before each test is run. Thus, in each test I will have a fresh instance of the request object:
let request = null;
beforeEach(() => {
request = new Request();
});
I implement this functionality in the query class, add a method to it that saves the settings in a class instance variable, as the test demonstrates.
configure(options){
this.options = options;
}
I run test execution and now I see two green points. Two of my tests have been successfully completed. But. It is assumed, however, that my queries will be addressed to the database. Now it is probably worth trying to see from which side the request will receive information about the database. I will return to the tests and write some code:
const DbMock = require('./DbMock');
let db = null;
beforeEach(() => {
db = new DbMock();
request = new Request(db);
});
It seems to me that such a classic version of initialization through the constructor fully satisfies my current requirements.
Naturally, I am not going to use in the unit tests an interface to the real MySQL database with which our project works. Why? Because:
- If, instead of me, someone from my colleagues will need to work on this part of the project, and perform unit tests before they can do anything, they will have to spend time and energy to install and set up their own MySQL server instance.
- The success of the unit tests will depend on the correctness of the pre-filling data used by the MySQL server database.
- The time it takes to run tests using a MySQL database will be significantly longer.
Okay. And why, for example, not to use any database in memory in the unit tests? It will work quickly, and the process of its configuration and initialization can be automated. All right, but at the moment I do not see any benefits from using this additional tool. It seems to me that my current needs faster and cheaper (no need to spend time learning) can be satisfied by means of classes and methods заглушек
, and псевдообъектов
that will only mimic the behavior of interfaces to be used in combat.
By the way. In combat conditions, I suggest using bookshelf in conjunction with knex . Why? Because following the documentation on installing, configuring and using these two tools, I managed to create and execute a database query in a few minutes.
What follows from this? From this it follows that I have to refine the class code Request
so that the execution of the query corresponds to the interfaces exported by my combat tools. So now the code should look like this:
classRequest{
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;
I will run the tests and see what happens. Yeah. Of course, I don’t have a module DbMock
, so the first thing I do is implement a stub for it:
classDbMock{
constructor(){}
}
module.exports = DbMock;
I will run the tests again. Now what? Now the princess Jasmine
tells me that mine DbMock
does not implement the property Model
. I'll try to think of something:
classDbMock{
constructor(){
this.Model = {
extend: () => {}
};
}
}
module.exports = DbMock;
Run tests again. Now the error is that in my unit test, I run the query execution, without having previously configured its parameters using the method configure
. I fix this:
const options = {
tableName: 'users',
query: { id: 1 }
};
it('execute should return promise', () => {
request.configure(options);
request.execute().then((result) => {
expect(result).toBeNull();
});
});
Since the instance of the variable options
I have already used in two tests, I put it in the initialization code of the entire test suite and run the tests again.
As expected, the method extend
, properties Model
, class DbMock
returned to us undefined
, in this regard, of course our query has no way to call the method where
.
I now understand that a property of a Model
class DbMock
should be implemented outside the class itself DbMock
. First of all, due to the fact that the implementation is заглушек
necessary for the existing tests to be executed, it will require too many nested scopes when the property is initialized Model
right in the class DbMock
. It will be absolutely impossible to read and understand ... And this, however, will not stop me from such an attempt, because I want to make sure that I still have the opportunity to write only a few lines of code and make the tests run successfully.
So. Inhale, exhale, nervous to leave the room. I supplement implementation of the designer DbMock
. Taaaaaammmm ....
classDbMock{
constructor(){
this.Model = {
extend: () => {
return {
where: () => {
return {
fetch: () => {
returnnewPromise((resolve, reject) => {
resolve(null);
});
}
};
}
};
}
};
}
}
module.exports = DbMock;
Tin! However, we run tests with a firm hand and make sure that the Jasmine
green spots show us again. And that means we are still on the right track, although something has inadmissibly swollen around us.
What's next? It can be seen with the naked eye that the Model
pseudo-database property should be implemented as something completely separate. Although offhand and not clear how it should be implemented.
But I absolutely know for sure that the records in this pseudo-base right now I will be stored in the most ordinary arrays. And since for the existing tests I need only a table imitation users
, for the beginning I will implement an array of users, with one record. But first, I will write a test:
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');
});
});
I run the tests. I am convinced that they do not pass, and I implement my simple container with the user:
const Users = [
{ Id: 1, Name: 'Jack' }
];
module.exports = Users;
Now the tests are performed, and it occurs to me that semantically Model
, in the package, bookshell
this is the provider of the interface to access the contents of the table in the database. Not for nothing, we extend
pass an object with the name of the table to the method . Why is it called extend
, and not for example get
, I do not know. Perhaps this is just a lack of API knowledge bookshell
.
Well, God bless him, for now I have an idea in my head about the following test:
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');
});
});
});
Since at the moment I need an implementation that only simulates the functionality of a real storage driver, I call the classes appropriately by adding a suffix Mock
:
classTableMock{
constructor(container){
this.container = container;
}
fetch() {
returnnewPromise((resolve, reject) => {
resolve(this.container[0]);
});
}
}
module.exports = TableMock;
But fetch
not the only method that I intend to use in the combat version, so I add another test:
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');
});
});
Running it, as it should be, displays an error message to me. So complement the implementation TableMock
method where
:
where(){
returnthis;
}
Now the tests are performed and you can move on to thinking about the implementation of properties Model
in the class DbMock
. As I already assumed, this would be some kind of instance provider for objects like 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();
});
});
Why TableMockMap
, because semantically this is it. Just instead of the method get
name, the method name is used extend
.
As the test falls, we do the implementation:
const Users = require('./Users');
const TableMock = require('./TableMock');
classTableMockMapextendsMap{
constructor(){
super();
this.set('users', Users);
}
extend(options){
const container = this.get(options.tableName);
returnnew TableMock(container);
}
}
module.exports = TableMockMap;
We run the tests and see six green points in the console. Life is Beautiful.
As it seems to me right now, it is already possible to get rid of страшной пирамиды
initialization in the class constructor DbMock
, using the wonderful one TableMockMap
. Let's not postpone it, especially since it would be nice to have tea already. The new implementation is exquisitely elegant:
const TableMockMap = require('./TableMockMap');
classDbMock{
constructor(){
this.Model = new TableMockMap();
}
}
module.exports = DbMock;
Run tests ... and oops! Our most important test falls. But this is even good, because it was a test stub and now we just have to fix it:
it('execute should return promise', () => {
request.configure(options);
request.execute().then((result) => {
expect(result.Id).toEqual(1);
expect(result.Name).toEqual('Jack');
});
});
Tests completed successfully. And now you can take a break, and then return to finalizing the resulting request code, because it is still very, very far from perfect, but even from an easy-to-use interface, despite the fact that the data from it bases can already be received.