DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
Securing Your Software Supply Chain with JFrog and Azure
Register Today

Trending

  • SRE vs. DevOps
  • How to Use an Anti-Corruption Layer Pattern for Improved Microservices Communication
  • How To Use Git Cherry-Pick to Apply Selected Commits
  • Deploying Smart Contract on Ethereum Blockchain

Trending

  • SRE vs. DevOps
  • How to Use an Anti-Corruption Layer Pattern for Improved Microservices Communication
  • How To Use Git Cherry-Pick to Apply Selected Commits
  • Deploying Smart Contract on Ethereum Blockchain
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. Table-driven Tests in Go

Table-driven Tests in Go

For a Java programmer, transitioning to Go can evoke "Where's My JUnit?" Fortunately Go has both a built-in testing library and a very smooth way to write tests idiomatically.

Alan Hohn user avatar by
Alan Hohn
·
Dec. 11, 15 · Opinion
Like (1)
Save
Tweet
Share
4.00K Views

Join the DZone community and get the full member experience.

Join For Free

I've started working in Go both professionally and personally and have enjoyed the experience. One reason is that Go takes a quite different approach to Java in some areas, which makes switching between the two a mind-expanding experience.

One such area is in unit testing. Go puts enough value on unit tests to make testing a part of the standard library (including code coverage, which I intend to discuss in the future). But the approach is different from a library like JUnit in that there are no assertions. Instead there are just functions like Errorf to fail the test with a log message.

To see the full example from this article, see this small GitHub repository. It contains a function that returns the numeric value for a Roman numeral in string form. 

Unit tests in Go are stored in files that end in _test.go. These files will not be built with the normal code, but will be inspected for unit tests. In these files, we write functions starting with Test that take a single parameter, of type *testing.T. For example:

import "testing"

...

func TestValid(t *testing.T) {
   // Run tests
}

The function does not need a return type; if the function ends as expected, the test passes. To fail the test, we use functions on the *testing.T type. As mentioned above, this type does not provide "assertion" style functions; instead there are functions that log errors and fail the test. For this reason, Go tests use regular if/else expressions, such as:

if err != nil {
    t.Errorf("Unexpected error for input %v: %v", tt.input, err)
}

This is more verbose than an assertion, but it has the advantage of being more explicit. The code is comprehensible to anyone who understands the language, and doesn't require learning a separate library. It also aligns test code with regular Go code, since checking for things like non-nil errors is a standard practice in Go (in place of the exception handling seen in other languages). For more information about Go's built-in support for unit testing, see this article, also on DZone.

To make up for the extra verbosity in checking for errors, Go encourages table driven testing. The idea is to build a data structure, usually a slice, that contains test inputs and expected outputs, then to iterate over the slice, testing each case.

While this kind of table-driven test is easy to do in other languages, such as Java, it is made very simple in Go by the ability to declare and populate data structures in a single statement.

For example, to test a function that takes a string and returns an int, we can declare the following slice:

var validTests = []struct {
    input    string
    expected int
}{
    {"", 0},
    {"I", 1},
}

The equivalent in Java would probably be done by declaring a class, then creating a static initializer to build a collection of instances of that class. Not difficult, but more verbose.

The code to iterate over the table can be basic; of course, it needs to be tailored to reflect how the code under test should be initialized and called.

for _, tt := range validTests {
    res, err := RomanToInt(tt.input)
    if err != nil {
        t.Errorf("Unexpected error for input %v: %v", tt.input, err)
    }
    if res != tt.expected {
        t.Errorf("Unexpected value for input %v: %v", tt.input, res)
    }
}

This code takes advantage of Go's simple iteration over slices. The use of for _, tt allows us to ignore the index of each item and just use the item itself.

The best part about table-driven tests is that, once the test is written, we can ignore the test method and just add cases to the table. Also, the input and expected output are easily visible and associated together in the test file. The downside is that we have to be careful crafting error messages to make sure that it is clear exactly which test case is failing. It is also a bit more difficult to create breakpoints on specific test cases.

So far I've just shown a table-driven test for valid inputs and outputs. In a future article I'll show a table-driven test to perform error checking as well.

unit test

Opinions expressed by DZone contributors are their own.

Trending

  • SRE vs. DevOps
  • How to Use an Anti-Corruption Layer Pattern for Improved Microservices Communication
  • How To Use Git Cherry-Pick to Apply Selected Commits
  • Deploying Smart Contract on Ethereum Blockchain

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com

Let's be friends: