Skip to content

Commit 194f70f

Browse files
authored
add tst module (#22)
* add tst module * unittest ErrorIs * codecov for tst * lint fixes * add unittests for ErrorOfType * add unittest of ErrorStringContains * unit tests * readme * omit thelper lint in some tests
1 parent 3e92832 commit 194f70f

File tree

12 files changed

+1057
-0
lines changed

12 files changed

+1057
-0
lines changed

.github/dependabot.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,17 @@ updates:
5050
main:
5151
patterns:
5252
- "*"
53+
54+
- package-ecosystem: "gomod"
55+
directory: "/tst"
56+
schedule:
57+
interval: "daily"
58+
commit-message:
59+
prefix: "[tst]"
60+
include: "scope"
61+
allow:
62+
- dependency-type: all
63+
groups:
64+
main:
65+
patterns:
66+
- "*"

.github/workflows/sub_tst.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
2+
name: tst
3+
4+
on:
5+
workflow_dispatch: {}
6+
7+
push:
8+
branches: [ main ]
9+
10+
pull_request:
11+
branches: [ main ]
12+
paths:
13+
- .golangci.yml
14+
- tools/**
15+
- .github/workflows/ci.yml
16+
- .github/workflows/sub_tst.yml
17+
- tst/**
18+
19+
jobs:
20+
21+
ci:
22+
uses: ./.github/workflows/ci.yml
23+
with:
24+
mod_path: tst
25+
secrets:
26+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,16 @@ A [knadh/koanf](github.com/knadh/koanf) boilerplate with some helpers.
2727
[![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=conf)](https://codecov.io/gh/ifnotnil/x)
2828

2929
Install: `go get -u github.com/ifnotnil/x/conf`
30+
31+
## tst
32+
33+
A test helper package providing error assertion test functions.
34+
35+
[Readme](tst/README.md)    
36+
[![ci](https://github.com/ifnotnil/x/actions/workflows/sub_tst.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_tst.yml)
37+
[![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/tst)](https://goreportcard.com/report/github.com/ifnotnil/x/tst)
38+
[![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/tst)](https://pkg.go.dev/github.com/ifnotnil/x/tst)
39+
[![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=tst%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/tst?tab=versions)
40+
[![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=tst)](https://codecov.io/gh/ifnotnil/x)
41+
42+
Install: `go get -u github.com/ifnotnil/x/tst`

codecov.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ component_management:
3030
name: conf
3131
paths:
3232
- "conf/"
33+
- component_id: tst
34+
name: tst
35+
paths:
36+
- "tst/"
3337

3438
ignore:
3539
- "http/internal/testingx" # test helpers

tst/.mockery.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# https://vektra.github.io/mockery/v3.5/configuration/
2+
3+
log-level: info
4+
formatter: goimports
5+
force-file-write: true
6+
require-template-schema-exists: true
7+
8+
all: true
9+
recursive: false
10+
dir: '{{.InterfaceDir}}'
11+
filename: mocks_test.go
12+
pkgname: '{{.SrcPackageName}}'
13+
structname: '{{.Mock}}{{.InterfaceName}}'
14+
15+
# https://vektra.github.io/mockery/v3.5/template/
16+
template: testify
17+
template-schema: '{{.Template}}.schema.json'
18+
19+
packages:
20+
github.com/ifnotnil/x/tst:
21+
config:
22+
all: true
23+
recursive: true

tst/Makefile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
SHELL := /usr/bin/env bash
2+
3+
REPO_ROOT = $(shell cd .. && pwd)
4+
5+
include $(REPO_ROOT)/scripts/go.mk
6+
include $(REPO_ROOT)/tools/tools.mk
7+
include $(REPO_ROOT)/scripts/lib.mk
8+
9+
.PHONY: nginx
10+
nginx:
11+
@docker compose --file ./compress/testdata/nginx/compose.yml up --force-recreate --detach
12+
13+
.PHONY: nginx-down
14+
nginx-down:
15+
@docker compose --file ./compress/testdata/nginx/compose.yml down --volumes --remove-orphans --rmi local
16+
17+
.PHONY: test-integration
18+
test-integration: TAGS=integration
19+
test-integration: nginx test

tst/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# tst
2+
[![ci](https://github.com/ifnotnil/x/actions/workflows/sub_tst.yml/badge.svg)](https://github.com/ifnotnil/x/actions/workflows/sub_tst.yml)
3+
[![Go Report Card](https://goreportcard.com/badge/github.com/ifnotnil/x/tst)](https://goreportcard.com/report/github.com/ifnotnil/x/tst)
4+
[![PkgGoDev](https://pkg.go.dev/badge/github.com/ifnotnit/x/tst)](https://pkg.go.dev/github.com/ifnotnil/x/tst)
5+
[![Version](https://img.shields.io/github/v/tag/ifnotnil/x?filter=tst%2F*)](https://pkg.go.dev/github.com/ifnotnil/x/tst?tab=versions)
6+
[![codecov](https://codecov.io/gh/ifnotnil/x/graph/badge.svg?token=n0t9q5Y3Sf&component=tst)](https://codecov.io/gh/ifnotnil/x)
7+
8+
9+
Error asserting functions for table driven testcases.
10+
11+
Examples:
12+
```golang
13+
err := errors.New("not found")
14+
15+
tests := []struct {
16+
input error
17+
asserter ErrorAssertionFunc
18+
}{
19+
{
20+
input: nil,
21+
asserter: NoError(),
22+
},
23+
{
24+
input: err,
25+
asserter: Error(),
26+
},
27+
{
28+
input: err,
29+
asserter: ErrorIs(err),
30+
},
31+
{
32+
input: fmt.Errorf("wrapped %w", err),
33+
asserter: ErrorIs(err),
34+
},
35+
{
36+
input: &os.PathError{},
37+
asserter: ErrorOfType[*os.PathError](),
38+
},
39+
{
40+
input: &os.PathError{Op: "op", Path: "/abc", Err: os.ErrInvalid},
41+
asserter: ErrorOfType[*os.PathError](
42+
func(tt TestingT, pe *os.PathError) { assert.Equal(tt, "op", pe.Op) },
43+
),
44+
},
45+
{
46+
input: fmt.Errorf("wrapped %w", &os.PathError{Op: "op", Path: "/abc", Err: os.ErrInvalid}),
47+
asserter: All(
48+
Error(),
49+
ErrorIs(os.ErrInvalid),
50+
ErrorOfType[*os.PathError](
51+
func(tt TestingT, pe *os.PathError) { assert.Equal(tt, "op", pe.Op) },
52+
),
53+
ErrorStringContains("op"),
54+
),
55+
},
56+
}
57+
```

tst/errors.go

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package tst
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"reflect"
7+
"strings"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
const expectedError = "Expected error but none received"
14+
15+
type TestingT interface {
16+
Errorf(format string, args ...interface{})
17+
FailNow()
18+
}
19+
20+
type ErrorAssertionFunc func(t TestingT, err error) bool
21+
22+
func (e ErrorAssertionFunc) AsRequire() require.ErrorAssertionFunc {
23+
return func(tt require.TestingT, err error, _ ...any) {
24+
if suc := e(tt, err); !suc {
25+
tt.FailNow()
26+
}
27+
}
28+
}
29+
30+
func (e ErrorAssertionFunc) AsAssert() assert.ErrorAssertionFunc {
31+
return func(tt assert.TestingT, err error, _ ...any) bool {
32+
t, is := tt.(TestingT)
33+
if is {
34+
return e(t, err)
35+
}
36+
37+
// not possible
38+
tt.Errorf("Wrong TestingT type %T", tt)
39+
return false
40+
}
41+
}
42+
43+
func NoError() ErrorAssertionFunc {
44+
return func(t TestingT, err error) bool {
45+
if h, ok := t.(interface{ Helper() }); ok {
46+
h.Helper()
47+
}
48+
49+
if err != nil {
50+
t.Errorf("Expected nil error but received : %T(%s)", err, err.Error())
51+
return false
52+
}
53+
54+
return true
55+
}
56+
}
57+
58+
func Error() ErrorAssertionFunc {
59+
return func(t TestingT, err error) bool {
60+
if h, ok := t.(interface{ Helper() }); ok {
61+
h.Helper()
62+
}
63+
64+
if err == nil {
65+
t.Errorf(expectedError)
66+
return false
67+
}
68+
69+
return true
70+
}
71+
}
72+
73+
// ErrorIs returns an ErrorAssertionFunc that checks if the given error matches
74+
// any of the expected errors using errors.Is.
75+
// If no expected errors are provided, it simply checks that an error is present (similar to Error()).
76+
// Returns false if the error is nil or doesn't match any expected errors.
77+
func ErrorIs(allExpectedErrors ...error) ErrorAssertionFunc {
78+
return func(t TestingT, err error) bool {
79+
if h, ok := t.(interface{ Helper() }); ok {
80+
h.Helper()
81+
}
82+
if err == nil {
83+
t.Errorf(expectedError)
84+
return false
85+
}
86+
87+
if len(allExpectedErrors) == 0 {
88+
return true
89+
}
90+
91+
suc := true
92+
notMatched := make([]error, 0, len(allExpectedErrors))
93+
for _, expected := range allExpectedErrors {
94+
if !errors.Is(err, expected) {
95+
notMatched = append(notMatched, expected)
96+
suc = false
97+
}
98+
}
99+
100+
if !suc {
101+
sb := strings.Builder{}
102+
sb.WriteString("Error is unexpected.\n")
103+
sb.WriteString(fmt.Sprintf("Got error : %T(%s)\n", err, err.Error()))
104+
105+
if len(notMatched) == 1 {
106+
sb.WriteString(fmt.Sprintf("Expected error : %T(%s)\n", notMatched[0], notMatched[0].Error()))
107+
t.Errorf(sb.String())
108+
return suc
109+
}
110+
111+
sb.WriteString("Expected errors:\n")
112+
for _, e := range notMatched {
113+
sb.WriteString(fmt.Sprintf(" -> %T(%s)\n", e, e.Error()))
114+
}
115+
t.Errorf(sb.String())
116+
}
117+
118+
return suc
119+
}
120+
}
121+
122+
func ErrorOfType[T error](typedAsserts ...func(TestingT, T)) ErrorAssertionFunc {
123+
return func(t TestingT, err error) bool {
124+
if h, ok := t.(interface{ Helper() }); ok {
125+
h.Helper()
126+
}
127+
128+
if err == nil {
129+
t.Errorf(expectedError)
130+
return false
131+
}
132+
133+
var wantErr T
134+
if !errors.As(err, &wantErr) {
135+
var tErr T
136+
t.Errorf("Error type check failed.\nExpected error type: %T\nGot : %T(%s)", tErr, err, err)
137+
return false
138+
}
139+
140+
if v := reflect.ValueOf(wantErr); v.Kind() == reflect.Pointer && v.IsNil() {
141+
t.Errorf("Error check failed.\nExpected not nill error value: %T\nGot : %T(nil)", wantErr, wantErr)
142+
return false
143+
}
144+
145+
for _, e := range typedAsserts {
146+
e(t, wantErr)
147+
}
148+
149+
return true
150+
}
151+
}
152+
153+
func ErrorStringContains(s string) ErrorAssertionFunc {
154+
return func(t TestingT, err error) bool {
155+
if h, ok := t.(interface{ Helper() }); ok {
156+
h.Helper()
157+
}
158+
159+
if err == nil {
160+
t.Errorf(expectedError)
161+
return false
162+
}
163+
164+
// consider case insensitive?
165+
if !strings.Contains(err.Error(), s) {
166+
t.Errorf("Error string check failed. \nExpected to contain: %s\nGot : %s\n", s, err.Error())
167+
return false
168+
}
169+
170+
return true
171+
}
172+
}
173+
174+
func All(expected ...ErrorAssertionFunc) ErrorAssertionFunc {
175+
return func(t TestingT, err error) bool {
176+
if h, ok := t.(interface{ Helper() }); ok {
177+
h.Helper()
178+
}
179+
180+
ret := true
181+
for _, fn := range expected {
182+
ok := fn(t, err)
183+
if !ok {
184+
ret = ok
185+
}
186+
}
187+
188+
return ret
189+
}
190+
}

0 commit comments

Comments
 (0)