Unit Test and Integration Test
Background
It is the first time that I try to write an article in English.
In the past, I didn’t write test code. Just thinking QA is responsible for testing.
As a developer, I don’t need to care about tests.
Although I know tests are essential, I can’t be aware of their importance.
After I joined my current team, it is required for developers to write tests.
So I have to write tests code.
After 8 months, I realize that tests are critical. I need to enforce my ability of testing.
therefore, I recently read a book Vladimir Khorikov - Unit Testing Principles Practices and Patterns
.
Learned a lot of unit test and integration best practice and code design.
So I want to share some knowledge with you.
Test
There are two kinds of tests.
- Unit Test
- Integration Test
- end to end test
Test Coverage
Do we need a 100% test coverage?
No. We don’t.
For trivial code, we can ignore that. Because they aren’t worth it.
We should focus on our business logic.
If a metric shows that there’s too little coverage in your code base—say, only 10%—
that’s a good indication that you are not testing enough.
But the reverse isn’t true:
even 100% coverage isn’t a guarantee that you have a good-quality test suite. A test
suite that provides high coverage can still be of poor quality.
Unit Test
The goal of unit test is to enable sustainable project growth.
Just as all tests are not created equal, not all parts of your code base are worth the
same attention in terms of unit testing.
Unit test should test a unit of behavior rather than code.
The four pillars of a good unit test
- Protection against regressions
- Resistance to refactoring
- Fast feedback
- Maintainability
Protection against regressions
The more features you develop, the more chances there are that you’ll break one of those features with a new release.
Sometimes We don’t awake that the new release will break one existing feature. Even with QA regression test, it also happens many times.
If we have a good automation test, can reduce these situations.
So good tests should protect against regressions.
To maximize the metric of protection against regressions, the test needs to aim at exercising as much code as possible.
Resistance to refactoring
Resistance to refactoring — the degree to which a test can sustain a refactoring of the underlying application code without turning red (failing).
This attribute can give us confidence to refactor.
How can we do?
We shouldn’t test the details of code. We should test the observable behavior.
Aim at the end result instead of implementation details
There is a example:
# we shouldn't care what's detail that we get from the data
test "get user by id" do
assert "select * from users where id = 1" == User.get(1).to_sql
end
# we just need to verify the data
test "get user by id" do
assert User.get(1).name == "Steven"
assert User.get(1).id == 1
end
Fast feedback
Fast feedback brings a excellent experience.
Picture the situation, we run a test, it takes 1 minute to show you result.
I can’t stand it.
Our project has hundreds of thousands of code. Run slowly and waste my time to wait for result.
How to run faster?
With less communications of out of process.
Async and parallel execution.
Maintainability
Using plain English as test titles.
Simple phrases in plain English do a much better job: they are more expressive
and don’t box you in a rigid naming structure. With simple phrases, you can describe
the system behavior in a way that’s meaningful to a customer or a domain expert.
how to write code to easy test
Split business logic and out of process communications (side effects).
def check do
if User.admin? do
:error
else
:ok
end
end
def check_with_side_effect do
if User.admin? do
# side effect
AuditLog.record()
:error
else
:ok
end
end
With side effects, it is hard to compose code and hard to test.
Pure function is easiest to test.
Integration Test
Using integration tests to verify the behavior of the system as a whole.
Mock
Integration test verifies database, third-party api, mq and so on.
All out-of-process dependencies fall into two categories:
- Managed dependencies (out-of-process dependencies you have full control over)—These
dependencies are only accessible through your application; interactions with
them aren’t visible to the external world. A typical example is a database. External systems normally don’t access your database directly; they do that through
the API your application provides. - Unmanaged dependencies (out-of-process dependencies you don’t have full control over)—
Interactions with such dependencies are observable externally. Examples include
an SMTP server and a message bus: both produce side effects visible to other
applications.
For those out-of-process dependencies, we should mock unmanaged dependencies.
Roles of Test
- Trivial code: we shouldn’t test, it isn’t worth it.
- Domain model , algorithms: Unit test carefully test
- Controllers: Integration test, but doesn’t need to test all situation. Happy path and edge cases are enough.
- Overcomplicated code: We should reduce these code. Split it.
Test Pyramid
Recap
It simply summary Unit Testing.
Talk is cheap, we should write more code.