| // Convenience utilities for testing. |
| package testutils |
| |
| import ( |
| "encoding/json" |
| "flag" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path" |
| "reflect" |
| "runtime" |
| "testing" |
| |
| "github.com/davecgh/go-spew/spew" |
| assert "github.com/stretchr/testify/require" |
| ) |
| |
| const ( |
| SMALL_TEST = "small" |
| MEDIUM_TEST = "medium" |
| LARGE_TEST = "large" |
| ) |
| |
| var ( |
| small = flag.Bool(SMALL_TEST, false, "Whether or not to run small tests.") |
| medium = flag.Bool(MEDIUM_TEST, false, "Whether or not to run medium tests.") |
| large = flag.Bool(LARGE_TEST, false, "Whether or not to run large tests.") |
| uncategorized = flag.Bool("uncategorized", false, "Only run uncategorized tests.") |
| |
| // DEFAULT_RUN indicates whether the given test type runs by default |
| // when no filter flag is specified. |
| DEFAULT_RUN = map[string]bool{ |
| SMALL_TEST: true, |
| MEDIUM_TEST: true, |
| LARGE_TEST: true, |
| } |
| |
| TIMEOUT_SMALL = "4s" |
| TIMEOUT_MEDIUM = "15s" |
| TIMEOUT_LARGE = "10m" |
| |
| // TEST_TYPES lists all of the types of tests. |
| TEST_TYPES = []string{ |
| SMALL_TEST, |
| MEDIUM_TEST, |
| LARGE_TEST, |
| } |
| ) |
| |
| // ShouldRun determines whether the test should run based on the provided flags. |
| func ShouldRun(testType string) bool { |
| if *uncategorized { |
| return false |
| } |
| |
| // Fallback if no test filter is specified. |
| if !*small && !*medium && !*large { |
| return DEFAULT_RUN[testType] |
| } |
| |
| switch testType { |
| case SMALL_TEST: |
| return *small |
| case MEDIUM_TEST: |
| return *medium |
| case LARGE_TEST: |
| return *large |
| } |
| return false |
| } |
| |
| // SmallTest is a function which should be called at the beginning of a small |
| // test: A test (under 2 seconds) with no dependencies on external databases, |
| // networks, etc. |
| func SmallTest(t *testing.T) { |
| if !ShouldRun(SMALL_TEST) { |
| t.Skip("Not running small tests.") |
| } |
| } |
| |
| // MediumTest is a function which should be called at the beginning of an |
| // medium-sized test: a test (2-15 seconds) which has dependencies on external |
| // databases, networks, etc. |
| func MediumTest(t *testing.T) { |
| if !ShouldRun(MEDIUM_TEST) { |
| t.Skip("Not running medium tests.") |
| } |
| } |
| |
| // LargeTest is a function which should be called at the beginning of a large |
| // test: a test (> 15 seconds) with significant reliance on external |
| // dependencies which makes it too slow or flaky to run as part of the normal |
| // test suite. |
| func LargeTest(t *testing.T) { |
| if !ShouldRun(LARGE_TEST) { |
| t.Skip("Not running large tests.") |
| } |
| } |
| |
| // SkipIfShort causes the test to be skipped when running with -short. |
| func SkipIfShort(t *testing.T) { |
| if testing.Short() { |
| t.Skip("Skipping test with -short") |
| } |
| } |
| |
| // AssertDeepEqual fails the test if the two objects do not pass reflect.DeepEqual. |
| func AssertDeepEqual(t *testing.T, a, b interface{}) { |
| if !reflect.DeepEqual(a, b) { |
| assert.FailNow(t, fmt.Sprintf("Objects do not match: \na:\n%s\n\nb:\n%s\n", spew.Sprint(a), spew.Sprint(b))) |
| } |
| } |
| |
| // AssertCopy is AssertDeepEqual but also checks that none of the direct fields |
| // have a zero value and none of the direct fields point to the same object. |
| // This catches regressions where a new field is added without adding that field |
| // to the Copy method. Arguments must be structs. |
| func AssertCopy(t *testing.T, a, b interface{}) { |
| AssertDeepEqual(t, a, b) |
| |
| // Check that all fields are non-zero. |
| va := reflect.ValueOf(a) |
| vb := reflect.ValueOf(b) |
| assert.Equal(t, va.Type(), vb.Type(), "Arguments are different types.") |
| for va.Kind() == reflect.Ptr { |
| assert.Equal(t, reflect.Ptr, vb.Kind(), "Arguments are different types (pointer vs. non-pointer)") |
| va = va.Elem() |
| vb = vb.Elem() |
| } |
| assert.Equal(t, reflect.Struct, va.Kind(), "Not a struct or pointer to struct.") |
| assert.Equal(t, reflect.Struct, vb.Kind(), "Arguments are different types (pointer vs. non-pointer)") |
| for i := 0; i < va.NumField(); i++ { |
| fa := va.Field(i) |
| z := reflect.Zero(fa.Type()) |
| if reflect.DeepEqual(fa.Interface(), z.Interface()) { |
| assert.FailNow(t, fmt.Sprintf("Missing field %q (or set to zero value).", va.Type().Field(i).Name)) |
| } |
| if fa.Kind() == reflect.Map || fa.Kind() == reflect.Ptr || fa.Kind() == reflect.Slice { |
| fb := vb.Field(i) |
| assert.NotEqual(t, fa.Pointer(), fb.Pointer(), "Field %q not deep-copied.", va.Type().Field(i).Name) |
| } |
| } |
| } |
| |
| // TestDataDir returns the path to the caller's testdata directory, which |
| // is assumed to be "<path to caller dir>/testdata". |
| func TestDataDir() (string, error) { |
| _, thisFile, _, ok := runtime.Caller(0) |
| if !ok { |
| return "", fmt.Errorf("Could not find test data dir: runtime.Caller() failed.") |
| } |
| for skip := 0; ; skip++ { |
| _, file, _, ok := runtime.Caller(skip) |
| if !ok { |
| return "", fmt.Errorf("Could not find test data dir: runtime.Caller() failed.") |
| } |
| if file != thisFile { |
| return path.Join(path.Dir(file), "testdata"), nil |
| } |
| } |
| } |
| |
| func readFile(filename string) (io.Reader, error) { |
| dir, err := TestDataDir() |
| if err != nil { |
| return nil, fmt.Errorf("Could not read %s: %v", filename, err) |
| } |
| f, err := os.Open(path.Join(dir, filename)) |
| if err != nil { |
| return nil, fmt.Errorf("Could not read %s: %v", filename, err) |
| } |
| return f, nil |
| } |
| |
| // ReadFile reads a file from the caller's testdata directory. |
| func ReadFile(filename string) (string, error) { |
| f, err := readFile(filename) |
| if err != nil { |
| return "", err |
| } |
| b, err := ioutil.ReadAll(f) |
| if err != nil { |
| return "", fmt.Errorf("Could not read %s: %v", filename, err) |
| } |
| return string(b), nil |
| } |
| |
| // MustReadFile reads a file from the caller's testdata directory and panics on |
| // error. |
| func MustReadFile(filename string) string { |
| s, err := ReadFile(filename) |
| if err != nil { |
| panic(err) |
| } |
| return s |
| } |
| |
| // ReadJsonFile reads a JSON file from the caller's testdata directory into the |
| // given interface. |
| func ReadJsonFile(filename string, dest interface{}) error { |
| f, err := readFile(filename) |
| if err != nil { |
| return err |
| } |
| return json.NewDecoder(f).Decode(dest) |
| } |
| |
| // MustReadJsonFile reads a JSON file from the caller's testdata directory into |
| // the given interface and panics on error. |
| func MustReadJsonFile(filename string, dest interface{}) { |
| if err := ReadJsonFile(filename, dest); err != nil { |
| panic(err) |
| } |
| } |
| |
| // WriteFile writes the given contents to the given file path, reporting any |
| // error. |
| func WriteFile(t assert.TestingT, filename, contents string) { |
| assert.NoError(t, ioutil.WriteFile(filename, []byte(contents), os.ModePerm)) |
| } |
| |
| // CloseInTest takes an ioutil.Closer and Closes it, reporting any error. |
| func CloseInTest(t assert.TestingT, c io.Closer) { |
| if err := c.Close(); err != nil { |
| t.Errorf("Failed to Close(): %v", err) |
| } |
| } |
| |
| // AssertCloses takes an ioutil.Closer and asserts that it closes. |
| func AssertCloses(t assert.TestingT, c io.Closer) { |
| assert.NoError(t, c.Close()) |
| } |
| |
| // Remove attempts to remove the given file and asserts that no error is returned. |
| func Remove(t assert.TestingT, fp string) { |
| assert.NoError(t, os.Remove(fp)) |
| } |
| |
| // RemoveAll attempts to remove the given directory and asserts that no error is returned. |
| func RemoveAll(t assert.TestingT, fp string) { |
| assert.NoError(t, os.RemoveAll(fp)) |
| } |
| |
| // TempDir is a wrapper for ioutil.TempDir. Returns the path to the directory and a cleanup |
| // function to defer. |
| func TempDir(t assert.TestingT) (string, func()) { |
| d, err := ioutil.TempDir("", "testutils") |
| assert.NoError(t, err) |
| return d, func() { |
| RemoveAll(t, d) |
| } |
| } |
| |
| // MarshalJSON encodes the given interface to a JSON string. |
| func MarshalJSON(t *testing.T, i interface{}) string { |
| b, err := json.Marshal(i) |
| assert.NoError(t, err) |
| return string(b) |
| } |
| |
| // MarshalIndentJSON encodes the given interface to an indented JSON string. |
| func MarshalIndentJSON(t *testing.T, i interface{}) string { |
| b, err := json.MarshalIndent(i, "", " ") |
| assert.NoError(t, err) |
| return string(b) |
| } |