📜 ⬆️ ⬇️

Developing a team to request data from the database - part 2

In the previous part, I dwelled on the fact that the team I was developing implements behavior that can be described with this test:


it('execute should return promise', () => { request.configure(options); request.execute().then((result) => { expect(result.Id).toEqual(1); expect(result.Name).toEqual('Jack'); }); }); 

As it seems to me now, getting in the form of a Promise result and processing it is not exactly what I would like. It would be better if the team itself performed this routine work, and the result would be placed for example in the Redux repository. I will try to rewrite the existing test to express my new expectations in it:


 const store = require('../../src/store'); const DbMock = require('../mocks/DbMock'); const db = new DbMock(); const Request = require('../../src/storage/Request'); const options = { tableName: 'users', query: { Id: 1 } }; let request = null; beforeEach(() => { request = new Request(db, store); }); it('should dispatch action if record exists', () => { request.configure(options); request.execute(() => { const user = store.getState().user; expect(user.Id).toEqual(options.query.Id); expect(user.Name).toEqual('Jack'); }); }); 

This is probably more convenient, despite the fact that I now have to teach the execute method, the Request class, to execute the callback method if the user passes it as an argument. One cannot do without it, because inside execute I intend to use asynchronous calls, which can only be tested by being convinced that their execution is complete.


Further ... Looking at the first line of the code, I understand that before I can return to editing the Request class code, I need to add a Redux package to the project, implement at least one редуктор and implement separately the packaging of this gear in the Store . The first test will be for the gearbox perhaps:


 const reduce = require('../../src/reducers/user'); it('should return new state', () => { const user = { Id: 1, Name: 'Jack'}; const state = reduce(null, {type: 'USER', user }); expect(state).toEqual(user); }); 

I run tests and agree with Jasmine that in addition to all previous errors, a module with the name ../../src/reducers/user not found. Therefore, I will write it, especially since it promises to be tiny and terribly predictable:


 const user = (state = null, action) => { switch (action.type) { case 'USER': return action.user; default: return state; } }; module.exports = user; 

I run the tests and I do not see radical improvements. This is because the module ../../src/store , the existence of which I suggested in the test for my class Request , I still have not implemented. Yes, and the test for it itself is also not yet. I'll start of course with the test:


 describe('store', () => { const store = require('../src/store'); it('should reduce USER', () => { const user = { Id: 1, Name: 'Jack' }; store.dispatch({type: 'USER', user }); const state = store.getState(); expect(state.user).toEqual(user); }); }); 

Tests? There are more messages about the absence of the store module, so I’ll deal with them immediately.


 const createStore = require('redux').createStore; const combineReducers = require('redux').combineReducers; const user = require('./reducers/user'); const reducers = combineReducers({user}); const store = createStore(reducers); module.exports = store; 

Understanding that I will have more than one reducer, I run a little ahead in the implementation of the хранилища and use the combineReducers method for its assembly. I run the tests again and see a new error message that tells me that the Request method of my class Request does not work as my test suggests. As a result of executing the execute method, the user record does not appear in the хранилище . It's time for the Request class to refactor.


Let me recall how the execute method test looks like now:


 it('should dispatch action if record exists', () => { request.configure(options); request.execute(() => { const user = store.getState().user; expect(user.Id).toEqual(options.query.Id); expect(user.Name).toEqual('Jack'); }); }); 

And I will correct the code of the method itself so that the test has a chance to execute:


 execute(callback){ const table = this.db.Model.extend({ tableName: this.options.tableName }); table.where(this.options.query).fetch().then((item) => { this.store.dispatch({ type: 'USER', user: item }); if(typeof callback === 'function') callback(); }); } 

I'll type in the npm test console and ... Bingo! My request has learned not only to receive data from the database, but also to store it in the контейнере состояния future processing process, so that subsequent operations can receive this data without any problems.


But! My handler can dispatch only one type of action per контейнер состояния , and this severely limits its capabilities. I want to use this code again whenever I need to extract some record from the database and dispatch it to the cell of the контейнера состояния for further processing under the key I need. And so I begin to refactor the test again:


 const options = { tableName: 'users', query: { Id : 1 }, success (result, store) { const type = 'USER'; const action = { type , user: result }; store.dispatch(action); } }; it('should dispatch action if record exists', () => { request.configure(options); request.execute(() => { const user = store.getState().user; expect(user.Id).toEqual(options.query.Id); expect(user.Name).toEqual('Jack'); }); }); 

It occurred to me that it would be nice to rid the Request class of unusual functionality for processing the results of the request. Semantically Request is a request. They fulfilled the request, received an answer, the task is completed, the principle of the class’s sole responsibility is observed. And let them be specially trained by the processing of the results, whose sole responsibility assumes some version of the actual processing. Therefore, I decided in the request settings to pass the method success , which is the task of processing successfully returned data by the request.


Tests, now you can not run. Mind, I understand that. I did not fix anything in the test itself and did not change anything in the implementation and the tests should continue to run successfully. But emotionally, I need to execute the npm test command and I execute it, and go to editing the implementation of my execute method in the Request class to replace the line with the store.dispatch(...) call, with the line with the this.options.success(...) call this.options.success(...) :


 execute(callback){ const table = this.db.Model.extend({ tableName: this.options.tableName }); table.where(this.options.query).fetch().then((item) => { this.options.success(item, this.store); if(typeof callback !== 'undefined') callback(); }); } 

I run the tests. Voila! Tests are absolutely green. Life is getting better! What's next? Immediately I see that I need to change the title of the test, because it does not quite correspond to reality. The test checks not that the method dispatches as a result of the request, but that the state updates the container as a result of the request. Therefore, changing the test title for ... well, for example:


 it('should update store user state if record exists', () => { request.configure(options); request.execute(() => { const user = store.getState().user; expect(user.Id).toEqual(options.query.Id); expect(user.Name).toEqual('Jack'); }); }); 

What's next? And then I think the time has come to pay attention to the case when, instead of the requested data, my request returns an error. This is not such an impossible scenario. True? And most importantly, in this case, I will not be able to prepare and send the required data set to my KYC operator, for the sake of integration with which I am writing all this code. It is so? So. First, I will write a test:


 it('should add item to store error state', () => { options.query = { Id: 555 }; options.error = (error, store) => { const type = 'ERROR'; const action = { type, error }; store.dispatch(action); }; request.configure(options); request.execute(() => { const error = store.getState().error; expect(Array.isArray(error)).toBeTruthy(); expect(error.length).toEqual(1); expect(error[0].message).toEqual('Something goes wrong!'); }); }); 

I don’t know if the test structure shows that I decided to save time and money and write a minimum of code to check the case when the request returns an error? No visible?


I don’t want to waste time encoding additional implementations of TableMock that will simulate errors. I decided that at the moment a couple of conditional constructions in the existing implementation would be enough for me, and I assumed that this could be adjusted through query query parameters. So, my assumptions:



Of course this is far from ideal. Much more readable and convenient for perception would be to implement the corresponding instances of DbMock , well, for example, HealthyDbMock , FaultyDbMock , EmptyDbMock . From the names of which it is immediately clear that the first will always work correctly, the second will always work incorrectly, but about the third one can assume that it will always return null instead of the result. Perhaps, having checked my first assumptions in the aforesaid way, that it seems to me that it will take a minimum of time, I will deal with the implementation of two additional instances of DbMock , imitating unhealthy behavior.


I run the tests. I get the expected error of the absence of the property I need in the контейнере состояния and ... I am writing another test. This time for a reducer that will handle actions with an ERROR type.


 describe('error', () => { const reduce = require('../../src/reducers/error'); it('should add error to state array', () => { const type = 'ERROR'; const error = new Error('Oooops!'); const state = reduce(undefined, { type, error }); expect(Array.isArray(state)).toBeTruthy(); expect(state.length).toEqual(1); expect(state.includes(error)).toBeTruthy(); }); }); 

Run tests again. All is expected, one more was added to the existing errors. Implement редуктор :


 const error = (state = [], action) => { switch (action.type) { case 'ERROR': return state.concat([action.error]); default: return state; } }; module.exports = error; 

I run the tests again. The new gearbox works as expected, but I still have to make sure that it connects to the repository and processes the actions for which it is intended. Therefore, I am writing an additional test for the existing storage test suite:


 it('should reduce error', () => { const type = 'ERROR'; const error = new Error('Oooops!'); store.dispatch({ type, error }); const state = store.getState(); expect(Array.isArray(state.error)).toBeTruthy(); expect(state.error.length).toEqual(1); expect(state.error.includes(error)).toBeTruthy(); }); 

I run the tests. Everything is expected. The action with the ERROR type does not process the existing storage. Modifying the existing storage initialization code:


 const createStore = require('redux').createStore; const combineReducers = require('redux').combineReducers; const user = require('./reducers/user'); const error = require('./reducers/error'); const reducers = combineReducers({ error, user }); const store = createStore(reducers); module.exports = store; 

For the hundredth time he threw a net ... Very good! Now the storage accumulates the received error messages in a separate property of the container.


Now I will add a couple of conditional constructions to the existing implementation of TableMock , thus teaching it to bounce some queries returning an error. The updated code looks like this:


 class TableMock { constructor(array){ this.container = array; } where(query){ this.query = query; return this; } fetch(){ return new Promise((resolve, reject) => { if(this.query.Id === 1) return resolve(this.container[0]); if(this.query.Id === 555) return reject(new Error('Something goes wrong!')); }); } } module.exports = TableMock; 

I run the tests and get a message about the raw rejection of the Promise in the Request method's execute method. I add the missing code:


 execute(callback){ const table = this.db.Model.extend({ tableName: this.options.tableName }); table.where(this.options.query).fetch().then((item) => { this.options.success(item, this.store); if(typeof callback === 'function') callback(); }).catch((error) => { this.options.error(error, this.store); if(typeof callback === 'function') callback(); }); } 

And I run the tests again. AND??? There is actually no test for the execute method, the Request class, this one:


 it('should add item to store error state', () => { options.query = { Id: 555 }; options.error = (error, store) => { const type = 'ERROR'; const action = { type, error }; store.dispatch(action); }; request.configure(options); request.execute(() => { const error = store.getState().error; expect(Array.isArray(error)).toBeTruthy(); expect(error.length).toEqual(1); expect(error[0].message).toEqual('Something goes wrong!'); }); }); 

He successfully completed. So the query functionality in terms of error handling can be considered implemented. Another test fell, the one that checks the performance of the storage in error handling. The problem is that my module with the storage implementation returns the same static copy of the storage to all consumers in all tests. In this regard, since the dispatching of errors occurs already in two tests, in one of them the check of the number of errors in the container does not necessarily pass. Because by the time the test is run, there is already one error in the container, and another one is added to the test run process. Therefore, this code:


 const error = store.getState().error; expect(error.length).toEqual(1); 

Throws an exception, telling that the expression error.length is actually 2, not 1. I will solve this problem right now simply by transferring the storage initialization code directly to the storage test initialization code:


 describe('store', () => { const createStore = require('redux').createStore; const combineReducers = require('redux').combineReducers; const user = require('../src/reducers/user'); const error = require('../src/reducers/error'); const reducers = combineReducers({ error, user }); const store = createStore(reducers); it('should reduce USER', () => { const user = { Id: 1, Name: 'Jack' }; store.dispatch({type: 'USER', user }); const state = store.getState(); expect(state.user).toEqual(user); }); it('should reduce error', () => { const type = 'ERROR'; const error = new Error('Oooops!'); store.dispatch({ type, error }); const state = store.getState(); expect(Array.isArray(state.error)).toBeTruthy(); expect(state.error.length).toEqual(1); expect(state.error.includes(error)).toBeTruthy(); }); }); 

The test initialization code now looks a bit puffy, but I can well return to its refactoring later.


I run the tests. Voila! All tests are completed and you can take a break.



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