Integration testing

To recap till now, we described:

  1. How to design databases
  2. How to design API contracts
  3. How to design state management
  4. How to design components

Each step is dependent on its predecessor. Any fault in any step before it completely renders the code and database useless downstream because there is supposed to be a single source of truth, and maintaining it is crucial. Without it, your application is serving garbage to users. And then why would they pay? In this last stage, what we are doing is even after following all these steps to make sure our application maintains a single source of truth, we are trying to test it out.

Integration test checks if your application is running properly across all the steps. If the flow below is happening uninterrupted, and truth is being maintained.

User Action → Component → Protocol → State Update → API Call → Database → API Response → State Update → Selector → Component Re-render → User sees

That means you have to test out each and every component to figure out if the connection between two steps is working properly and exactly at which step is something going wrong. It does not test components, it does not test individual APIs or database tables. It tests the connections between layers so that the whole thing is running as it’s supposed to. In the above box, we have defined a flow. Its job is to figure out which arrow is not working well and if all arrows are working as intended. Unit tests just work locally. They tell you if the function is correct in its limited local context. But integration tests see if the chain of processes is manipulating information as it’s supposed to and is maintaining truth. It is asking if the truth is able to survive the travel that is happening between this whole workflow.

That’s why a proper definition of a contract (be it front-end or back-end) is necessary because that’s how you can properly test integration. Otherwise, you will have to guess which endpoints or protocols exist. Unit tests tell you if something is locally right or wrong. Integration tests tell you if the whole process is globally right and where the failure is arising locally.

We think about database design in terms of entities.

We think about API contracts in terms of actions.

We think about state management in terms of the needs of the user.

So, how do we design integration tests?

Integration tests are designed in terms of the intention of the system. Like, whatever happened, was it in alignment with the contract? Did it declare what it was supposed to? And did it take the correct path? That means we need to have a pure workflow or data flow map which draws out the intended map and each action that is supposed to happen at each step. And then we test it out. If there is a deviation between reality and intention, where the intention is the correct flow and correct declarations at state so there is traceability. Thus we can say that integration tests are done in terms of actions, consequences and intents. You have this action with this certain intent. So you commit the action and if you see if the intent was fulfilled.

So I guess first of all, you need to create a proper map and then make a list of actions. How do you make a proper map and how do you make a list of these actions?

Well, this can be a hell to map out everything, so we will have to take the help of the Pareto principle that 80% of the user journeys are going to be just 20% of the possible ones. So you identify the most critical journeys and then map it out.

Things like:

  1. Register a new account
  2. Purchase a product
  3. Publish a blog
  4. Run code analysis

Or it can be sequential also. Like, example: You are making a social media app. Then you can say:

  1. The login stage
  2. The user data forms
  3. The preference about their feed
  4. Them viewing the feed
  5. Them liking it

Then there can be interaction with other users that hey someone can view someone else’s profile and follow them and see their posts. Basically, when you have clarity of your own idea, you can map out user workflows easily. And then for each one of them, you can see if this workflow was followed.

User Action → Component → Protocol → State Update → API Call → Database → API Response → State Update → Selector → Component Re-render → User sees
  1. You make a list of actions
  2. You make a map that should happen
  3. You write the test that goes end-to-end
  4. You compare the results

There can be certain actions that are totally back-and-side and automated. Stuff like clean-ups or backups etc. They can also be boiled down to:

Trigger → API/Service Call → Database → Response → Evaluation → Closure

Not all integration points are equal. Use this framework:

CriticalityTest CoverageExample
P0: TransactionalFull chain + rollbackPurchases, financial transactions
P1: Core FeatureHappy + major error pathsPosts, comments, messages
P2: Secondary FlowsHappy path onlyProfile edits, settings changes
P3: EnhancementCritical sub-paths onlyAnimations, sort preferences

Basically, we are here to answer the question that when the user intends to do X and does some action Y, does our system’s response honor all the contracts between the UI, the database, the backend.