跳转至

How to write Unit Test in Go

Overview

Go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Specify("Default Test: Testing InitUser/GetUser on a single user.", func() {
    userlib.DebugMsg("Initializing user Alice.")
    alice, err = client.InitUser("alice", defaultPassword)
    Expect(err).To(BeNil())

    userlib.DebugMsg("Getting user Alice.")
    aliceLaptop, err = client.GetUser("alice", defaultPassword)
    Expect(err).To(BeNil())
})

BeforeEach(func() {
    // This runs before each test within this Describe block (including nested tests).
    // Here, we reset the state of Datastore and Keystore so that tests do not interfere with each other.
    // We also initialize
    userlib.DatastoreClear()
    userlib.KeystoreClear()
})

Specify("FileOpe Test: Test StoreFile", func() {
    // Basic Idea:
    // Store this file and check if its return value is nil

    // Init
    userlib.DebugMsg("Initializing user Bob.")
    bob, err := client.InitUser("bob", defaultPassword)
    Expect(err).To(BeNil())

    // Store File
    userlib.DebugMsg("Storing file data: %s", contentOne)
    err = bob.StoreFile(bobFile, []byte(contentOne))
    Expect(err).To(BeNil())
})
  1. Each Spcify block is a test case for one feature.
  2. BeforeEach block is used to reset the state of Datastore and Keystore so that tests do not interfere with each other.

Therefore, we use BeforeEach to reset and divide different tests which is specified by Specify block.

Test Syntax

We take this block as an example:

Go
1
2
3
4
5
6
7
8
9
Specify("Default Test: Testing InitUser/GetUser on a single user.", func() {
    userlib.DebugMsg("Initializing user Alice.")
    alice, err = client.InitUser("alice", defaultPassword)
    Expect(err).To(BeNil())

    userlib.DebugMsg("Getting user Alice.")
    aliceLaptop, err = client.GetUser("alice", defaultPassword)
    Expect(err).To(BeNil())
})

Test name should be clear and concise, and should be able to describe the tested feature

  1. The test name is Default Test: Testing InitUser/GetUser on a single user.
  2. The test name is usually corresponding to the feature to be tested, here it means "We are going to test InitUser and GetUser".

Use Error Return Value to check if the specific function works correctly

nil

In Go, nil is a pre-declared identifier representing the zero value of a pointer, channel, func, interface, map, or slice type.

If return value equals to nil, it means that the function works correctly. (like return 0 in C++)

Let take a deep look into this test body:

  1. userlib.DebugMsg("Initializing user Alice.")
    • This is a debug message, which is used to print out the message to the console.
    • "Hey buddy, we are going to initialize user Alice."
  2. alice, err = client.InitUser("alice", defaultPassword)
    • This is the function we want to test, client.InitUser(a,b).
    • 2 return values: alice and err.
    • If err is nil, it means that the function works correctly.
  3. We expect err to be nil, so we use Expect(err).To(BeNil()) to check if it is nil.

Therefore, in this case, if err == nil, and Expect(err).To(BeNil()) works well, and it means that function client.InitUser(a,b) works correctly. Now we go to userlib.DebugMsg("Getting user Alice.") for a sequent test. We are all good!

But, if err != nil, and Expect(err).To(BeNil()) fails, it means that function client.InitUser(a,b) does not work correctly. And the test will fail.

Failure Msg in Go

When a test fails in Go (especially using the Ginkgo testing framework, as seems to be the case here), an error message will typically be printed to the console, making it clear that an issue occurred. Here’s what the user will see:

  1. Failure Message: Ginkgo will show a message like "Expected nil, but got ", where <error> details the specific error returned by the function. This message helps the user quickly identify that the Expect(err).To(BeNil()) check did not pass.

  2. Test Name: Ginkgo will display the name of the failed test case, such as "Default Test: Testing InitUser/GetUser on a single user.", so the user knows exactly which test encountered the problem.

  3. Debug Messages: Any userlib.DebugMsg statements that were executed before the failure will also be visible in the output, helping to trace where the failure occurred.

  4. Additional Context: In some cases, the line number in the code where the failure occurred will be shown. This can guide the user to the exact point of failure.

Like:

Go
1
2
3
4
 Failure [0.005 seconds]
Default Test: Testing InitUser/GetUser on a single user.
Expected nil, but got error: <specific error message ...>
/path/to/testfile_test.go:42

Q & A

Q1 Expect a function to return a specific error

One more question is that: we expect a function to return a specific error, especially in a function which is responsible for checking in original code (avoid illegal action), how to do that?

Go
1
2
3
4
// Append to Nonexistent File is illegitimate
userlib.DebugMsg("Appending file data: %s", contentOne)
err = alice.AppendToFile(aliceFile, []byte(contentOne)) // In this scenario, aliceFile does not exist.
Expect(err).ToNot(BeNil())

In this case, we don't allow appending to a nonexistent File (Product's feature).

Hence, if we append to a nonexisting file by AppendToFile, we expect alice.AppendToFile(aliceFile, []byte(contentOne)) to return an error, so we use Expect(err).ToNot(BeNil()) to check if it is not nil.

Q2 Not only error checking, but also check the return data

We can not only check error, but also check the return data.

Go
1
2
3
4
userlib.DebugMsg("Checking that Bob sees expected file data.")
data, err = bob.LoadFile(bobFile)
Expect(err).To(BeNil())
Expect(data).To(Equal([]byte(contentOne + contentTwo + contentThree)))

Like Except(data).To(Equal([]byte(contentOne + contentTwo + contentThree))), we can check the return data of the function.

In this way, we can check verify the function working status not only by checking the error, but also by checking the return data.