📜 ⬆️ ⬇️

Developing a team to request data from the database

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 is essentially a команда that returns a Promise with the result? From this it is possible to begin. I jot down the code so that the test can be run.


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

I run the npm test and watch in the console the green dot of the test that was executed.


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 the request variable, I will initialize 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:


  1. 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.
  2. The success of the unit tests will depend on the correctness of the pre-filling data used by the MySQL server database.
  3. 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 are faster and cheaper (no need to spend time studying) can be satisfied using classes and methods of заглушек and псевдообъектов -objects, which will only imitate the behavior of interfaces that are supposed to be used in combat conditions.


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 modify the Request class code so that the execution of the request corresponds to the interfaces exported to my combat tools. So now the code should look like this:


 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; 

I will run the tests and see what happens. Yeah. DbMock course, I don’t have a DbMock module, so the first thing I do is implement a stub for it:


 class DbMock { constructor(){} } module.exports = DbMock; 

I will run the tests again. Now what? Now Princess Jasmine tells me that my DbMock does not implement the Model property. I'll try to think of something:


 class DbMock { 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 configure method. 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 I have already used an instance of the options variable in two tests, I put it into the initialization code of the entire test suite and run the tests again.


As expected, the extend method, the Model properties, of the DbMock class returned us undefined , so naturally our query has no way to call the where method.


I already understand that the Model property, the DbMock class, should be implemented outside of the DbMock class DbMock . First of all, due to the fact that the implementation of the заглушек necessary for the existing tests to be executed, will require too many nested scopes when initializing the Model property right in the DbMock class. 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 DbMock implementation of the designer of DbMock . Taaaaaammmm ....


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

Tin! However, we run tests with a firm hand and make sure that Jasmine again shows us green dots. And that means we are still on the right track, although something has inadmissibly swollen around us.


What's next? The naked eye can see that the Model property of a pseudo-database 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 an imitation of the users table, then for a start 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, the Model , in the bookshell package, is the provider of the access interface to the contents of the table in the database. Not for nothing, we pass the object with the name of the table to the extend method. Why it is called extend , and not for example get , I do not know. Perhaps this is just a lack of knowledge about the bookshell API.


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, adding the Mock suffix:


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

But fetch not the only method 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 I supplement the implementation of TableMock with the where method:


 where(){ return this; } 

Now the tests are performed and you can move on to reflections on the implementation of the Model property in the DbMock class. As I have already assumed, this will be some kind of provider of instances of objects of the type 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 name of the get method, use the extend method name.


As the test falls, we do the implementation:


 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; 

We run the tests and see six green points in the console. Life is Beautiful.


As it seems to me right now, you can get rid of the страшной пирамиды initialization in the constructor of the DbMock class, using the wonderful 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'); class DbMock { 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.



Source: https://habr.com/ru/post/436228/