Code have different purposes, sometime significantly (logic/algorithm vs storage access/store) and interact with different type of resources. So, to have a good and effective test coverage but also practically testing them we need a different type of tests for different kind of code. Below are the type of testings that we do to address them.
This type of test exercise the computational/logic part of the software which makes it the test with the least cost (easy to setup, fast to run) but the most value (can easily cover a lot of test cases and logic variations). It is quite obvious then that majority of the test should be of this type.
To be unit-testable, our code need to be decoupled from external dependencies like I/O, storage or services so it can be tested independently i.e: have a proper boundaries/contract with the external world.
This is the part where we test parts outside the boundaries as mentioned earlier. Given a good coverage on the unit tests, this part only need to tests whether I/O and other kind of services give proper response given certain request. The aim is to ensure this layer give a proper output to be used as input by our logic above.
This test check that all the parts run together as fully functioning use case after being tested independently as the above. It is an end-to-end test and being done as close as possible to how user actually using it. It is to show and prove the feature works. The UI part that is purely rendering is tested here also which make it sometime we refer to it as UI test.
We are focusing here on how everything fit and works together. Thus, we no longer tested logic and variations that already tested on the previous tests, at least not extensively and for different purpose and audience. For example on testing the date is displayed we only need to some sanity assertions e.g: tha value is there and not empty, but no longer check how the month and date string is formatted in detail which sould already be extensively tested in, much less costly, unit test.
Testing Pyramid and Clean Architecture
The type proportions of the test above typically refered to as Test Pyramid. There are some variations on what constitute Integration and Acceptance test but basically it is a guideline on how an effective composition of tests should be. It ensure we cover most of the thing without being impractical by having more of unit tests and more effective use of the more costly (but still necessary) type of test.
We can watch our code and test as we do them to more aligned with this proportion, but one natural way to get a good composition like above is following Clean Architecture. It’s an architectural guideline which has been around for quite sometime, at least in the core idea and with different name e.g: port-adapter, domai-driven-design. It basically put your use case and domain as the inner part of your code base while the rest e.g: I/O as external part that is separated cleanly by boundaries/contract.
Here are the parts on clean architecture that would be in unit test :
Interactor : application logic e.g: sort displayed item by date
Entity : business logic e.g: video that wins need to have x number of vote and min y number of play
Presenter : ui logic e.g: the field should be red when it’s in the wrong formatted
Boundaries : interface/contract for external dependencies (Storage, Service), not being tested by itself but used by the above items for testing as a contract for adapter implementation later on below. It also useful to make UI logic in Presenter can be tested independently without the need for concrete implementation for application logic in Interactor that, although fully unit testable, might require some elaborate setup
Integration Test is done on
Adapters component which is an implementation on boundaries/contract above.
Acceptance Test is typically done on fully-built application using UI testing framework available on each platform.
As you can see, most of your code (and its related test) would be behind the boundaries while small parts (I/0, rendering, integration) would be outside and generally in smaller amount.
It also show a nice side effect of having your code testable beside having your work easily verified and tested, it also makes your code more modular and each part logically separated e.g: you I/O code dooes not clutter your domain logic part.