How to write unit test

Using code to test code is harder than running a program manually.

Suppose you have finished some CRUD code, you want to verify the code is running correctly. Your code might be like this:

func foo(user, post, arrayOfComments)error{
    createUser(user)
    createPost(user.id, post)
    createComments(post.id, arrayOfComments)
}

How to test the foo function?

To ensure the function foo working correctly, we can build & run, and then trigger the function foo. Before running it, we should have a test environment that has the database connection and imported dependencies code as the same as the production. And we must construct the relation of the user, post and comment.

The foo function is not a stand-alone function, it must call the other create functions. After calling foo, we need to check the database record has been created successfully. Our unit test might like:

func testFoo(){
    err := foo(user, post, arrayOfComments)//call foo
    assert(err==nil)
    ensure_created(user)
    ensure_created(post)
    ensure_created(arrayOfComments)
}

Does it make sense?
No.
The affection for the database is not showing in foo's output. Our test case should not check the database. It's not the responsibility of the foo. We need to check the foo result and write some extra test cases for the other create functions.

func testFoo(){
    err := foo(user, post, arrayOfComments)//call foo
    assert(err==nil)
}

func TestUserCreate(){
    createUser(user)
    ensure_created(user)
}
func TestPostCreate(){
    createPost(post)
    ensure_created(post)
}
func TestCreateComments(){
    createComments(post.id, arrayOfComments)
    ensure_created(arrayOfComments)
}

Setting up a test environment is not easy. Because the code of our development might be:

In these situations, We didn't have so many choices, we might just as well write the simple calling test code:

func testFoo(){
    err := foo(user, post, arrayOfComments)//call foo
    assert(err==nil)
}

Does it has problems?
No.
Is it perfect?
No.

But it's better than no test, and the next time when you want to change the code or test it again, you can use this code repeatedly. Your code now is running independently. Even we just calling the foo, but it's worth writing the simple test case. It means we have a tiny running environment for our code. We don't need to build&run all code. This is the first step to construct the unit test environment.

Building and running code by hand is easy, but it's just one-time work. Using the unit test, you can build the fake data to run a function thousands of times with thousands kind of data types easily. The manual test just running at this time, we must construct the same test case again on the next time. It's not obeying the DRY(Don't repeat yourself) principle.

func testFoo(){
    for i:=0; i< 1000 i++{ //how to use your hand running a case 1000 times easily?
        err := foo(user, post, arrayOfComments)
        assert(err==nil)
    }
}

When I used to test my API with Postman, I'd always want the unit test can test my API automatically, and cleaning the database after all tests have been passed. This thought was wrong, test the calling chain with the unit test is not appropriate. I should find other tools to automate the scheduling. The unit test should be a small unit, not for the functions pipeline. In addition, You should not maintain the unit test running order. You should keep the unit test being simplicity and small.

If you code coupled with each other, divide it for several modules or functions for writing the unit test easily. If you can't decouple your code, just write the testing code for core features.

The automatic running of unit tests is not important, reusing the testing code is more important. That's why I didn't like TDD(test-driven development). Because code writing is not hard and does not need to spend too much time. Most of our time has been spending on requirement understanding and code debugging. TDD suppose you have known what you do and write the test before your production code. Sometimes you didn't understand the final requirement. For this purpose, your code should be easy to change(ETC). Your testing code is not the experiment code, it's like the production code and should keep readable, maintainable and flexible. It's the parts of your production.

When I tried to resolve the leet-code problems, it gave me feedback on the power of unit tests and forced me to write the code correctly. The runnable code is easy to write, but correct code is so hard to write. I found that if you only think about the running of your code, your code will not have been written solidly and abstractly. For passing all of the unit tests in leetcode, I must think about every possible branch of running. It's a good way to improve my coding skills. But the leetcode problems have the explicit input/output. When we are building the software, it's very rare to see the explicit input/output. The client's requirement is hard to understand completely, the requirement might have been changed tomorrow. Our hardware, network, and other components might not work at any time. Running our code correctly today and rerunning tomorrow is enough.