Sergei Basharov
Sergei Basharov

Reputation: 53850

How to implement BDD practices with standard Go testing package?

I want to write tests first, then write code that makes the tests pass.

I can write tests functions like this:

func TestCheckPassword(t *testing.T) {
    isCorrect := CheckPasswordHash("test", "$2a$14$rz.gZgh9CHhXQEfLfuSeRuRrR5uraTqLChRW7/Il62KNOQI9vjO2S")

    if isCorrect != true {
        t.Errorf("Password is wrong")
    }
}

But I'd like to have more descriptive information for each test function.

For example, I am thinking about creating auth module for my app. Now, in plain English, I can easily describe my requirements for this module:

  1. It should accept a non-empty string as input.
  2. String must be from 6 to 48 characters long.
  3. Function should return true if password string fits provided hash string and false if not.

What's the way to put this information that is understandable by a non-tech business person into tests besides putting them into comments?

Upvotes: 0

Views: 308

Answers (3)

icza
icza

Reputation: 417682

In Go, a common way of writing tests to perform related checks is to create a slice of test cases (which is referred to as the "table" and the method as "table-driven tests"), which we simply loop over and execute one-by-one.

A test case may have arbitrary properties, which is usually modeled by an anonymous struct.
If you want to provide a description for test cases, you can add an additional field to the struct describing a test case. This will serve both as documentation of the test case and as (part of the) output in case the test case would fail.

For simplicity, let's test the following simple Abs() function:

func Abs(x int) int {
    if x < 0 {
        return -x
    }
    return x
}

The implementation seems to be right and complete. If we'd want to write tests for this, normally we would add 2 test cases to cover the 2 possible branches: test when x is negative (x < 0), and when x is non-negative. In reality, it's often handy and recommended to also test the special 0 input and the corner cases: the min and max values of the input.

If we think about it, this Abs() function won't even give a correct result when called with the minimum value of int32, because that is -2147483648, and its absolute value is 2147483648 which doesn't fit into int32 because max value of int32 is: 2147483647. So the above implementation will overflow and incorrectly give the negative min value as the absolute of the negative min.

The test function that lists cases for each possible branches plus includes 0 and the corner cases, with descriptions:

func TestAbs(t *testing.T) {
    cases := []struct {
        desc string // Description of the test case
        x    int32  // Input value
        exp  int32  // Expected output value
    }{
        {
            desc: "Abs of positive numbers is the same",
            x:    1,
            exp:  1,
        },
        {
            desc: "Abs of 0 is 0",
            x:    0,
            exp:  0,
        },
        {
            desc: "Abs of negative numbers is -x",
            x:    -1,
            exp:  1,
        },
        {
            desc: "Corner case testing MaxInt32",
            x:    math.MaxInt32,
            exp:  math.MaxInt32,
        },
        {
            desc: "Corner case testing MinInt32, which overflows",
            x:    math.MinInt32,
            exp:  math.MinInt32,
        },
    }

    for _, c := range cases {
        got := Abs(c.x)
        if got != c.exp {
            t.Errorf("Expected: %d, got: %d, test case: %s", c.exp, got, c.desc)
        }
    }
}

Upvotes: 1

mbuechmann
mbuechmann

Reputation: 5760

If you want a test suite with descriptive texts and contexts (like rspec for ruby) you should check out ginko: https://onsi.github.io/ginkgo/

Upvotes: 0

Axel Wagner
Axel Wagner

Reputation: 194

In Go, the idiomatic way to write these kinds of tests is:

func TestCheckPassword(t *testing.T) {
    tcs := []struct {
        pw string
        hash string
        want bool
    }{
        {"test", "$2a$14$rz.gZgh9CHhXQEfLfuSeRuRrR5uraTqLChRW7/Il62KNOQI9vjO2S", true},
        {"foo", "$2a$14$rz.gZgh9CHhXQEfLfuSeRuRrR5uraTqLChRW7/Il62KNOQI9vjO2S", false},
        {"", "$2a$14$rz.gZgh9CHhXQEfLfuSeRuRrR5uraTqLChRW7/Il62KNOQI9vjO2S", false},
    }

    for _, tc := range tests {
        got := CheckPasswordHash(tc.pw, tc.hash)
        if got != tc.want {
            t.Errorf("CheckPasswordHash(%q, %q) = %v, want %v", tc.pw, tc.hash, got, want)
        }
    }
}

This is called "table-driven testing". You create a table of inputs and expected outputs, you iterate over that table and call your function and if the expected output does not match what you want, you write an error message describing the failure.

If what you want isn't as simple as comparing a return against a golden value - for example, you want to check that either an error, or a value is returned, or that a well-formed hash+salt is returned, but don't care what salt is used (as that's not part of the API), you'd write additional code for that - in the end, you simply write down what properties the result should have, add some if's to check that and provide a descriptive error message if the result is not as expected. So, say:

func Hash(pw string) (hash string, err error) {
    // Validate input, create salt, hash thing…
}

func TestHash(t *testing.T) {
    tcs := []struct{
        pw string
        wantError bool
    }{
        {"", true},
        {"foo", true},
        {"foobar", false},
        {"foobarbaz", true},
    }

    for _, tc := range tcs {
        got, err := Hash(tc.pw)
        if err != nil {
            if !tc.wantError {
                t.Errorf("Hash(%q) = %q, %v, want _, nil", tc.pw, got, err)
            }
            continue
        }
        if len(got) != 52 {
            t.Errorf("Hash(%q) = %q, want 52 character string", tc.pw, got)
        }
        if !CheckPasswordHash(tc.pw, got) {
            t.Errorf("CheckPasswordHash(Hash(%q)) = false, want true", tc.pw)
        }
    }
}

Upvotes: 0

Related Questions