Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

internal/testingiface: New testing.T equivalent interface and unit testing helpers #402

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions helper/logging/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"syscall"

"github.com/hashicorp/logutils"
"github.com/mitchellh/go-testing-interface"
"github.com/hashicorp/terraform-plugin-testing/internal/testingiface"
)

// These are the environmental variables that determine if we log, and if
Expand All @@ -33,7 +33,7 @@ var ValidLevels = []logutils.LogLevel{"TRACE", "DEBUG", "INFO", "WARN", "ERROR"}
// logging controlled by Terraform itself and managed with the TF_ACC_LOG_PATH
// environment variable. Calls to tflog.* will have their output managed by the
// tfsdklog sink.
func LogOutput(t testing.T) (logOutput io.Writer, err error) {
func LogOutput(t testingiface.T) (logOutput io.Writer, err error) {
logOutput = io.Discard

logLevel := LogLevel()
Expand Down Expand Up @@ -91,7 +91,7 @@ func LogOutput(t testing.T) (logOutput io.Writer, err error) {
// SetOutput checks for a log destination with LogOutput, and calls
// log.SetOutput with the result. If LogOutput returns nil, SetOutput uses
// io.Discard. Any error from LogOutout is fatal.
func SetOutput(t testing.T) {
func SetOutput(t testingiface.T) {
out, err := LogOutput(t)
if err != nil {
log.Fatal(err)
Expand Down
4 changes: 2 additions & 2 deletions helper/resource/plan_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import (
"errors"

tfjson "github.com/hashicorp/terraform-json"
"github.com/hashicorp/terraform-plugin-testing/internal/testingiface"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/mitchellh/go-testing-interface"
)

func runPlanChecks(ctx context.Context, t testing.T, plan *tfjson.Plan, planChecks []plancheck.PlanCheck) error {
func runPlanChecks(ctx context.Context, t testingiface.T, plan *tfjson.Plan, planChecks []plancheck.PlanCheck) error {
t.Helper()

var result []error
Expand Down
4 changes: 2 additions & 2 deletions helper/resource/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import (
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
"github.com/mitchellh/go-testing-interface"

"github.com/hashicorp/terraform-plugin-testing/internal/logging"
"github.com/hashicorp/terraform-plugin-testing/internal/plugintest"
"github.com/hashicorp/terraform-plugin-testing/internal/testingiface"
)

// protov5ProviderFactory is a function which is called to start a protocol
Expand Down Expand Up @@ -113,7 +113,7 @@ type providerFactories struct {
protov6 protov6ProviderFactories
}

func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *plugintest.WorkingDir, factories *providerFactories) error {
func runProviderCommand(ctx context.Context, t testingiface.T, f func() error, wd *plugintest.WorkingDir, factories *providerFactories) error {
// don't point to this as a test failure location
// point to whatever called it
t.Helper()
Expand Down
4 changes: 2 additions & 2 deletions helper/resource/state_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import (
"errors"

tfjson "github.com/hashicorp/terraform-json"
"github.com/mitchellh/go-testing-interface"

"github.com/hashicorp/terraform-plugin-testing/internal/testingiface"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
)

func runStateChecks(ctx context.Context, t testing.T, state *tfjson.State, stateChecks []statecheck.StateCheck) error {
func runStateChecks(ctx context.Context, t testingiface.T, state *tfjson.State, stateChecks []statecheck.StateCheck) error {
t.Helper()

var result []error
Expand Down
3 changes: 1 addition & 2 deletions helper/resource/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import (
"strings"
"time"

"github.com/mitchellh/go-testing-interface"

"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"github.com/hashicorp/terraform-plugin-go/tfprotov6"

Expand All @@ -31,6 +29,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/internal/addrs"
"github.com/hashicorp/terraform-plugin-testing/internal/logging"
"github.com/hashicorp/terraform-plugin-testing/internal/plugintest"
testing "github.com/hashicorp/terraform-plugin-testing/internal/testingiface"
)

// flagSweep is a flag available when running tests on the command line. It
Expand Down
4 changes: 2 additions & 2 deletions internal/logging/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

"github.com/hashicorp/terraform-plugin-log/tfsdklog"
helperlogging "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging"
testing "github.com/mitchellh/go-testing-interface"
"github.com/hashicorp/terraform-plugin-testing/internal/testingiface"
)

// InitContext creates SDK logger contexts when the provider is running in
Expand All @@ -34,7 +34,7 @@ func InitContext(ctx context.Context) context.Context {
// The standard library log package handling is important as provider code
// under test may be using that package or another logging library outside of
// terraform-plugin-log.
func InitTestContext(ctx context.Context, t testing.T) context.Context {
func InitTestContext(ctx context.Context, t testingiface.T) context.Context {
helperlogging.SetOutput(t)

ctx = tfsdklog.RegisterTestSink(ctx, t)
Expand Down
40 changes: 0 additions & 40 deletions internal/plugintest/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"path"
"path/filepath"
"strings"
"testing"
)

func symlinkFile(src string, dest string) error {
Expand Down Expand Up @@ -145,42 +144,3 @@ func CopyDir(src, dest, baseDirName string) error {

return nil
}

// TestExpectTFatal provides a wrapper for logic which should call
// (*testing.T).Fatal() or (*testing.T).Fatalf().
//
// Since we do not want the wrapping test to fail when an expected test error
// occurs, it is required that the testLogic passed in uses
// github.com/mitchellh/go-testing-interface.RuntimeT instead of the real
// *testing.T.
//
// If Fatal() or Fatalf() is not called in the logic, the real (*testing.T).Fatal() will
// be called to fail the test.
func TestExpectTFatal(t *testing.T, testLogic func()) {
t.Helper()

var recoverIface interface{}

func() {
defer func() {
recoverIface = recover()
}()

testLogic()
}()

if recoverIface == nil {
t.Fatalf("expected t.Fatal(), got none")
}

recoverStr, ok := recoverIface.(string)

if !ok {
t.Fatalf("expected string from recover(), got: %v (%T)", recoverIface, recoverIface)
}

// this string is hardcoded in github.com/mitchellh/go-testing-interface
if !strings.HasPrefix(recoverStr, "testing.T failed, see logs for output") {
t.Fatalf("expected t.Fatal(), got: %s", recoverStr)
}
}
28 changes: 28 additions & 0 deletions internal/testingiface/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

// Package testingiface provides wrapper types compatible with the Go standard
// library [testing] package. These wrappers are necessary for implementing
// [testing] package helpers, since the Go standard library implementation is
// not extensible and the existing code in this Go module is built on directly
// interacting with [testing] functionality, such as calling [testing.T.Fatal].
//
// The [T] interface has all methods of the [testing.T] type and the [MockT]
// type is a lightweight mock implementation of the [T] interface. There are a
// collection of assertion helper functions such as:
// - [ExpectFail]: That the test logic called the equivalent of
// [testing.T.Error] or [testing.T.Fatal].
// - [ExpectParallel]: That the test logic called the equivalent of
// [testing.T.Parallel] and passed.
// - [ExpectPass]: That the test logic did not call the equivalent of
// [testing.T.Skip], since [testing] marks these tests as passing.
// - [ExpectSkip]: That the test logic called the equivalent of
// [testing.T.Skip].
//
// This code in this package is intentionally internal and should not be exposed
// in the Go module API. It is compatible with the Go 1.17 [testing] package.
// It replaces the archived github.com/mitchellh/go-testing-interface Go module,
// but is implemented with different approaches that enable calls to behave
// more closely to the Go standard library, such as calling [runtime.Goexit]
// when skipping, and preserving any error/skip messaging.
package testingiface
143 changes: 143 additions & 0 deletions internal/testingiface/expect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package testingiface

import (
"sync"
)

// ExpectFailed provides a wrapper for test logic which should call any of the
// following:
// - [testing.T.Error]
// - [testing.T.Errorf]
// - [testing.T.Fatal]
// - [testing.T.Fatalf]
//
// If none of those were called, the real [testing.T.Fatal] is called to fail
// the test.
func ExpectFail(t T, logic func(*MockT)) {
t.Helper()

mockT := &MockT{}

var wg sync.WaitGroup
wg.Add(1)

go func() {
defer wg.Done()

logic(mockT)
}()

wg.Wait()

if mockT.Failed() {
return
}

t.Fatal("expected test failure")
}

// ExpectParallel provides a wrapper for test logic which should call the
// [testing.T.Parallel] method. If it doesn't, the real [testing.T.Fatal] is
// called.
func ExpectParallel(t T, logic func(*MockT)) {
t.Helper()

mockT := &MockT{}

var wg sync.WaitGroup
wg.Add(1)

go func() {
defer wg.Done()

logic(mockT)
}()

wg.Wait()

if mockT.Failed() {
t.Fatalf("unexpected test failure: %s", mockT.LastError())
}

if mockT.Skipped() {
t.Fatalf("unexpected test skip: %s", mockT.LastSkipped())
}

if mockT.IsParallel() {
return
}

t.Fatal("expected test parallel")
}

// ExpectPass provides a wrapper for test logic which should not call any of the
// following, which would mark the real test as passing:
// - [testing.T.Skip]
// - [testing.T.Skipf]
//
// If one of those were called, the real [testing.T.Fatal] is called to fail
// the test. This is only necessary to check for false positives with skipped
// tests.
func ExpectPass(t T, logic func(*MockT)) {
t.Helper()

mockT := &MockT{}

var wg sync.WaitGroup
wg.Add(1)

go func() {
defer wg.Done()

logic(mockT)
}()

wg.Wait()

if mockT.Failed() {
t.Fatalf("unexpected test failure: %s", mockT.LastError())
}

if mockT.Skipped() {
t.Fatalf("unexpected test skip: %s", mockT.LastSkipped())
}

// test passed as expected
}

// ExpectSkip provides a wrapper for test logic which should call any of the
// following:
// - [testing.T.Skip]
// - [testing.T.Skipf]
//
// If none of those were called, the real [testing.T.Fatal] is called to fail
// the test.
func ExpectSkip(t T, logic func(*MockT)) {
t.Helper()

mockT := &MockT{}

var wg sync.WaitGroup
wg.Add(1)

go func() {
defer wg.Done()

logic(mockT)
}()

wg.Wait()

if mockT.Failed() {
t.Fatalf("unexpected test failure: %s", mockT.LastError())
}

if mockT.Skipped() {
return
}

t.Fatal("test passed, expected test skip")
}
Loading
Loading