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

  • Tutorial

In the previous part, I stopped at the fact that the team I am developing implements behavior that can be described like this:


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 result Promiseand 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 storage Redux. 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');
  });
});

So perhaps it is more convenient, despite the fact that I now have to teach a executeclass Requestmethod to perform a callback method if the user passes it as an argument. One cannot do without it, because inside executeI intend to use asynchronous calls, which can be tested only if I am convinced that their execution has ended.


Further ... Looking at the first line of the code, I understand that before I can return to editing the class code Request, I need to add a package to the project Redux, implement at least one for it редукторand implement this gearbox package separately in 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 the tests and agree Jasminethat, in addition to all the previous errors, the module with the name was not found ../../src/reducers/user. 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 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 have not yet implemented. Yes, and the test for it itself is also not yet. I will begin 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? Reports of the absence of the module storehas become more, so I will 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 and use the method for its assembly combineReducers. I run the tests again and see a new error message that tells me that the method of executemy class Requestdoes not work as my test suggests. As a result of the method executein хранилищеdoes not appear on the entry user. It is time to refactor a class Request.


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


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 console npm testand ... Bingo! My request has learned not only to obtain data from the database, but also to store it in the контейнере состоянияfuture processing process, so that subsequent operations can obtain this data without problems.


But! My handler can dispatch to контейнер состоянияonly one type of action, and this greatly limits its capabilities. I want to use this code again whenever I need to extract some record from the database and dispatch it to a cell контейнера состояния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 save the class Requestfrom the unusual functionality for processing the results of the query. Semantically Requestis a query. They fulfilled the request, received an answer, the task is completed, the principle of the class’s sole responsibility is observed. And let someone specially trained in this, whose sole responsibility assume some kind of actual processing, let them process the results. Therefore, I decided to pass a method to the request settings success, which is the task of processing the data successfully returned 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 command npm testand I execute it, and turn to editing the implementation of my method executein the class Requestto replace the line with the call store.dispatch(...)with the line with the 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 does not check 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 on coding additional implementations TableMockthat 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 the request parameters query. So, my assumptions:


  • If Idthe query options.queryis 1 , then my pseudo table always returns resolved Promisewith the very first record from the collection.
  • If Idthe request options.queryis 555 , then my pseudo-table always returns rejected Promisewith an instance Errorinside, the content of messagewhich is equal to Something goes wrong! .

Of course, this is far from ideal. Much more readable and easier to read could implement appropriate instances 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 instead of the result null. Perhaps, having checked my first assumptions in the aforementioned way, that it seems to me that it will take a minimum of time, I will deal with the implementation of two additional instances DbMockimitating unhealthy behavior.


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


describe('error', () => {
  const reduce = require('../../src/reducers/error');
  it('should add error to state array', () => {
    const type = 'ERROR';
    const error = newError('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. Everything is expected, one more was added to the existing errors. I 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 = newError('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 type of the ERRORexisting storage does not handle. 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 store 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 TableMock, having taught it to bounce some queries thus returning an error. The updated code looks like this:


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

I run the tests and get a raw rejection message Promisein the executeclass method Request. 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 a method execute, a class Request, 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 storage implementation module returns the same static storage instance to all consumers in all tests. In this regard, since already in two tests error scheduling occurs, 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. Therefore, this code:


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

Throws an exception, saying that the expression error.lengthis actually 2, not 1. This problem right now I will solve by simply 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 = newError('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.


Also popular now: