Unit Testing — Data Access Architecture

Angular: The Full Gamut Edition

Charlie Greenman
December 05, 2020
7 min read

The Unit Testing Phenomenon

Unit testing in many enterprise settings is an interesting phenomenon. In particular, actual code for features will be very clean, maintainable, and stick to very particular conventions. However, unit testing can be a bit all over the place. This is one of a number of articles that will be written with regards to unit testing.


Interfaces and Unit Testing

In unit testing it can be very difficult to keep in sync the mocked data you are using, with actual data used within app’s actual live UI. The easiest, and most efficient way of doing this is creating interfaces. The part where it becomes tricky, is that generally data is used in multiple places. For instance, we might have state that is contained in a separate component, wherein the data it originates from can be somewhere else. In addition, the data might also be used in some other component as well as in some other service.


In Sync Data — Interface Architecture

An interface at its core is responsible for making sure that data follows a pre-described schema. The part that is counter-intuitive in an Angular setting, is what happens when you have services, components, state, and spec files all vying for the same data. Do we use one interface for all of them or different ones for each file group? If we do end up using one interface, what sort of data structure is it that we will use for all of these files.

Interface Architecture — The Dilemma

As we discussed in the previous paragraph, in an Angular application, there will be services, components, state, and spec files vying for the same data. The dilemma when it comes to interfaces, however, is that they need data in different ways. I would like to layout this data in detail. Let’s imagine that we have a form that we need to specify data for a grid that will be displayed:

  • Service — Used to determine the status of checkbox logic. It only needs to know the length of data, and actual data is irrelevant. Respective spec only needs to be aware of similar data.
  • Component — Needs to know actual data, so that it can pass along observable stream component Html. The respective spec needs to be aware of the data.
  • State — Depending on the reducer, or effect, it might need all of the data or none of it. The respective spec will need to familiar of similar data.

In Sync Data — The Solution

As one can see, it is actually counter-intuitive to create a singular interface for one’s service, state, component(s), and their respective specs. Primarily, because it would be stepping back and seeing how they would all interact with each other. However, if one does not use the same singular interface for all of them, one runs the risk of them getting out of sync with each other. The solution is as follows. There should be a singular interface that exists for the root of the following files:

  1. ServicesIncludes spec files
  2. Stateincludes spec files
  3. GraphQL(Interfaces not used, but influenced by, and therefore important to be in the same directory)
  4. Component(Not in the same folder as above, but will still use interface)

This, however, requires that all the files be tightly coupled together. In order to do this, we must create a well thought out folder/file structure. This is what we will be calling Data Access architecture.


Data Access Architecture — Folder Structure

I would like to point out that much consideration has gone into the following folder/file structure. It assumes that your component will hook into the backend, and use state management. How each part of it interacts with each other is for another time. However, right now the focus is on where the interface is located.

Screen Shot 2020-12-05 at 10.08.30 PM.png

There will be no module for services and instead we will use providedIn within our @Injectable decorator. For instance:

@Injectable({
  providedIn: 'root'
})

This will allow us to use the service in whatever module we would like, without having to import a module.

The respective component will contain the state. For instance:

Screen Shot 2020-12-05 at 10.10.01 PM.png

Now that we have proposed the data-access architecture, let’s run through it real quick and see how we might use the interface, to strongly couple data across our application.


Data Access Architecture -- Interface Deep Dive

Example of grid-form interface

export interface GridForm { 
  column: string;
  row: string;
  pixelSize: string;
}

This interface is going to be used across the entire data cycle.

Example of grid-form mock

const GridForm = { 
  column: '20';
  row: '20';
  pixelSize: '20';
}

Service for Pulling in Pre-Populated Gird Form

In our service, we will use the interface to determine what sort of data we expect to be pulled in.

getGridForm(projectId: string): Observable <GridForm> { 
  const query = GridFormQuery;
  const variables = {
    projectId
  };
  const form$ = this.apollo.query<GridForm>({query, variables});
  return from(buyers$).pipe(pluck('data')); 
}

Service Spec for Pulling in Pre-Populated Grid Form

Without going into detail of what unit tests it is that we will be writing, for service spec, we will use the grid-form mock, to make sure data passed in equals the data we expect. This is will use both mock and interface.

Reducer for populating state with appropriate Grid

For simplicity sake within our app, we are going to take the data as is, and pass it directly into our store to be used within app:

export function gridFormReducer(
  state: GridForm,
  action: BuyerAction
): GridForm {
  switch (action.type) {
    case BuyerTypes.FormLoaded: { 
      return { 
        ... state 
      };
    } 
  }
}

Here we have the interface telling our app that all data within our reducer must consist of the three items we have specified in our GridForm interface.

Reducer Spec for populating state with appropriate Grid

describe('gridFormLoaded action', () => {
  it('should populate the buyer entities and ids', () => {
      const action = new FormLoaded(gridForm);
      const state = gridFormReducer(initialState, action);
      expect(state).toEqual(gridForm);
  });
});

Here we are using a singular mock for our reducer spec. Staying true to our architecture, it the interface/mock changes in one place, it will affect all others.

Effect for populating state with appropriate Grid

@Effect()
loadGridForm$ = this.dataPersistence.fetch(BuyerTypes.LoadGridForm, {
  run: (action: LoadGridForm, state: GridForm) => {
    const projectId = this.projectFacade.getProjectIdFromState(state);
  return this.service
    .getGridForm(action.payload, projectId)
    .pipe(map((gridForm: GridForm) => new GridFormLoaded(gridForm)));
  },
onError: (action: LoadGridForm, error) => {
    console.error('Error', error);
  },
});

In our param for the state, the most data-heavy part of our effect, we are once again using the same interface of GridForm.

Effect Spec for populating state with appropriate Grid

const projectId = '123';
describe('loadGridForm$', () => {
  beforeEach(() => {
    spyOn(service, 'getGridForm').and.returnValue(of(gridForm));
  });
it('should work', () => {};
    const action = new LoadGridForm();
    const completion = new GridFormLoaded(gridForm);
actions$ = hot('-a', { a: action });
    const expected$ = cold('-c', { c: completion });
expect(effects.loadGridForm$).toBeObservable(expected$);
    expect(service.getGridForm).toHaveBeenCalledWith(projectId);
  });
});

Here we are attaching the mock for GridForm.

Component and Component Spec

They will not be contained within the same folder as the rest of the data-access pieces above. However, they will still the same interface and mock within the data-access folder.


Wrapping Up

There is obviously quite a bit of code above. Really it is just to prove the point that by having a singular interfaces.ts file, and a singular mocks.ts file, you can tightly couple the following:

  1. Services(Includes spec files)
  2. State(Includes spec files)
  3. GraphQL(Interfaces not used, but influenced by, and therefore important to be in the same directory)
  4. Component(Not in same folder as above, but will still use interface)

That is quite a powerful architecture.

It should be noted, that there is a chance of circular dependencies with this architecture. In which case, the workaround would be to treat the circular dependency as an exception with a new module.

Thank you for reading, and until the next one.

More articles similar to this

footer

Razroo is committed towards contributing to open source. Take the pledge towards open source by tweeting, #itaketherazroopledge to @_Razroo on twitter. One of our associates will get back to you and set you up with an open source project to work on.