0

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?

7
  • BDD is a pattern primarily for developing UIs. Your code doesn't appear to relate to a UI, so BDD likely doens't apply. Second, if you're writing an auth module, why do you need a non-technical explanation? Only technically inclined people use modules, typically. This really looks like a problem for plain old vanilla TDD. Commented Feb 20, 2018 at 10:24
  • I didn't see any information showing BDD is only for UI. Another thing, how to apply these human-readable explanations in another module that is more business-related? Commented Feb 20, 2018 at 10:27
  • The B in BDD stands for "Behavior". Software "behavior" only exists at a UI level. "Behavior" is what the end-user observes. Commented Feb 20, 2018 at 10:29
  • 2
    Write table driven tests and have one entry in the table with name "less than 6 characters long" and one with "more than 48 ..." you get it. Just take a look how the stdlib does it. Commented Feb 20, 2018 at 10:29
  • First, determine if human-readable explanations make sense. I don't think they do for your example, but I can't really be the one to judge that. Second, if you need human-readable explanations, you have two options: 1) A human-readable version, and an executable version, or 2) A human-readable executable version. Go does not provide the latter. Some popular BDD tools, like Cucumber, do. Commented Feb 20, 2018 at 10:30

3 Answers 3

1

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)
        }
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

Seems like the closest solution to my problem, thanks!
0

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)
        }
    }
}

Comments

0

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/

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.