4) Learnings from Unit Tests
--
Unit tests are important because they ensure all code meets quality standards set by the teams before it’s deployed to production. It is that point in entire development lifecycle with,
- High development gains by continuously optimising evolving metrics
- To build Confident & Self Evolving Teams
I’m specifically going to share my experience using 2 different class of high level problem space.
Case 1:
First class of problems are those that involve direct user interaction with product/service. We are making decisions/policies on behalf of user needs. These decisions are often derived from continuous experimentation & learning. Hence, it is likely that correct decisions will stick with Product for longtime. As user base(Hence Data) increases, it requires continuous evolution of Systems Architecture to meet the evolving user requirements right from Infrastructure layer.
Now that I have provided high level context of problem space, consider below example of Binary Search code snippet.
Below points can be inferred on the above function.
- Function is context free. This means it doesn’t contains any specific tightly coupled data points. We can use this function in any context we want to sort the objects. Hence this function can be placed in a common library and not in specific business layer codebase.
- Function expects input array to be in sorted order and not in random order.
- Function expects search ranges low & high to be positive numbers ≥ 0
- Function expects all the input data to be of type Integer. Not Floating point numbers & String type.
- Function expects max elements in the search array to be 2³²-1
- It has other constraints that are not ideal to associate those characteristics with Unit Testing.
As above function is not bound to any specific Context, we can use this function in any application specific context provided, above mentioned function constraints are well understood by the calling Context. Calling application Context will pass Context-Data to perform Searching task.
As you can see in the above business layer logic, it is updating product quantity. Main task contains 2 SubTasks within it.
SubTask1: Search Product by productId
SubTask2: Update Product Quantity
SubTask1 is Context-Free & we are passing data to search product and SubTask2 has Context passed by Parent & it is context-bounded since updating product is not really a common function. It may contains domain specific logic to validate the quantity before performing actual operation & also it may interact with persistent storage layer instead of interacting with in-memory structures. Hence we can’t make keep it as common library function. Once above task is complete, result is passed to Parent Context to complete whole request processing pipeline. As you can see from above examples,
- It is unlikely that subTask logic going continuous evolution since we know our product/service decisions are in best alignment with current user needs.
- It is important to note that, there is a Standard Input Model & Expected Output Model or Error for each SubTask involved in main Task.
- New subTasks might be added as part of logic evolution. In this case new Unit Tests are added separately to validate each new SubTask created.
Above lines make a strong point on why SubTask/function level Unit Testing can be done with 100% quality predictability. Only errors that should happen after component integration is Integration related problems which are less costly compared to Unit test issues.
case 2:
Second class of problems are those that involve two or more independent business units(Software Systems) communicating each other on pre-agreed Protocol. Each involving Business make decisions/policies independently which is in the best interest to them. This means Decision Makers here are more than one Business Unit. When integration is done with Such Systems, It results in communicating data-model being Dynamic(Unstructured Data) because Both participating Software Systems are evolving independently. It is not Just evolution of One Software Systems anymore, Because here we have both People & Systems involved.
- Systems talk to each other using Evolving Data-Model.
- People Interact with Systems Using IOT Devices. They are in the position to make decisions which is in the best interest of their Business. Mode of communication can be different too as Desktop/Web/Mobile etc.
- Data-Models are Unstructured & move continuously from one point to another point.
- Nature of Business Decisions between two communicating Systems start evolving because Businesses evolve too. It requires separating the concern of exposing Business Objects in the domain layer(Context-Data which is unstructured) to another upper layer where business rules can be implemented & evolved over time.
- Single unit of work in such case is, one complete Business Process(Well understood Protocol). This Process can contain multiple steps to achieve one single execution.
- Output of one task will be input to next task in the whole Process Execution chain. This point makes it clear that, It is difficult to say with full certainty what exactly is Input Data-Model for given task in the chain & What is the output Data-Model since Business rules that are part of given task keep evolving. Hence each task can be considered as Impure function.
Now that I have provided high level context of problem space, Let’s try to understand why Unit Tests are important.
Fig 1.c represents one full repeatable flow to be executed between 2 evolving Software Systems.
- If Unit tests are performed for Code which follows structure of Fig 1.a where there is no clear difference between layers for a single task, this may not produce good unit test cases. Since we are writing Unit Tests for a Single Task in a whole Process & Not for complete Process, layers(Business Rules + Input Data Model + Output Data Model) keep evolving. One of the Rules Of Unit Tests is writing Tests with 100% Predictability to gain confidence. If we write Unit Tests for Currently known Input Data Model & Expected Data Model, Existing Unit test cases will not sustain for longtime because Business Rules keep evolving. Hence as rules keep evolving we tend to focus on adding the Unit Tests to cover only New Business Rules. This may result in either breaking of existing Unit Tests or Unit Tests really not testing the case it is intended to do when first time it was written. This may also results in code coverage going down.
- If Unit tests are performed for Code which follows structure of Fig 1.b where there is clear distinction between layers, it is possible to write good unit test cases. Common Context free code is like any normal code which is not related to Business Rules. This code can be unit tested with 100% predictability like any other code as mentioned in case 1. It will not change as Business Rules change. Since Business Rules keep evolving, we cannot really test single task of whole flow as mentioned in Fig 1.c. If we test as mentioned in Point-1, then it is no different from above Point-1. What we really can do here is, test whole flow end to end(One Business use case by identifying Standard Input Model, Expected Output Model/Error) as Unit Tests. Each task is a different function, then how does it makes sense to test whole flow as Unit Test ? Doesn’t it fall under the Category of Integration test ?. My view on this is that, As Businesses Rules Evolve, we need to build Confidence that Predefined flows continue to work under predefined conditions. Every Branch/Line of code in the entire flow is there for a reason to satisfy certain use case. If Branch/Line is not covered, we should not think to test it at Task/Function level, instead our focus should be which specific condition in a use case flow really causes this Branch/Line to cover ? Then we test that particular flow as Unit Test. It is different from Integration tests in that sense where, Any low level data-access(Database layer) can be Mocked & Passed Appropriate Data-Models to satisfy particular use case. This way we can gain confidence that Unit Tests cover all the existing business use cases.