Skip to content

Commit c84b56e

Browse files
lowlighteryesnault
authored andcommitted
feat(assertions): support custom assertions
Signed-off-by: GitHub <noreply@github.com>
1 parent 793bfbf commit c84b56e

6 files changed

Lines changed: 159 additions & 0 deletions

File tree

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Venom is a CLI (Command Line Interface) that aims to create, manage and run your
3939
- [Assertions](#assertions)
4040
- [Keywords](#keywords)
4141
- [`Must` keywords](#must-keywords)
42+
- [Custom assertions](#custom-assertions)
4243
- [Using logical operators](#using-logical-operators)
4344
- [Write and run your first test suite](#write-and-run-your-first-test-suite)
4445
- [Export tests report](#export-tests-report)
@@ -695,6 +696,45 @@ Example:
695696
# Remaining steps in this context will not be executed
696697
```
697698

699+
#### Custom assertions
700+
701+
It is possible to use the [user defined executors syntax](#user-defined-executors) and prefixing your executor name with `Should` to create a new custom assertion keyword.
702+
703+
Example:
704+
```yml
705+
# lib/ShouldBeHttp2XX.yml
706+
executor: ShouldBeHttp2XX
707+
steps:
708+
- assertions:
709+
- a ShouldBeGreaterThanOrEqualTo 200
710+
- a ShouldBeLessThan 300
711+
```
712+
713+
You may also include additional steps like a regular user defined executor.
714+
715+
Custom assertions are executed in an entirely clean context, containing only the following variables:
716+
- `a`: the left operand
717+
- `b`: the (first) right operand
718+
- `argv`: the rights operands
719+
720+
If you need to be compatible with the `input` syntax of user defined executors, you could use the `argv` as the default value and access these through the regular `input.*` syntax.
721+
```yaml
722+
input:
723+
test: "{{.argv.argv1}}"
724+
```
725+
726+
To call a registered custom assertions, just use its registered name in the `assertions` array, like any built-in assertion keyword.
727+
728+
```yml
729+
# test.yml
730+
testcases:
731+
- steps:
732+
- type: http
733+
url: https://example.com
734+
assertions:
735+
- result.statuscode ShouldBeHttp2XX
736+
```
737+
698738
### Using logical operators
699739

700740
While assertions use `and` operator implicitly, it is possible to use other logical operators to perform complex assertions.

assertions/assertions.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,3 +1508,13 @@ func getTimeFromString(in interface{}) (time.Time, error) {
15081508

15091509
return t, nil
15101510
}
1511+
1512+
// RegisterCustomAssertFunc registers a custom assertion function
1513+
func RegisterCustomAssertFunc(name string, ux AssertFunc) error {
1514+
if _, ok := assertMap[name]; ok {
1515+
return errors.Errorf("cannot redefine existing assertion %q", name)
1516+
}
1517+
1518+
assertMap[name] = ux
1519+
return nil
1520+
}

assertions/assertions_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2438,3 +2438,37 @@ func TestShouldNotJSONEqual(t *testing.T) {
24382438
})
24392439
}
24402440
}
2441+
2442+
func TestRegisterCustomAssertFunc(t *testing.T) {
2443+
var assertFunc AssertFunc
2444+
2445+
tests := []struct {
2446+
name string
2447+
actual string
2448+
wantErr bool
2449+
}{
2450+
{
2451+
name: "register custom assertion",
2452+
actual: "ShouldCustom",
2453+
},
2454+
{
2455+
name: "redefine custom assertion error",
2456+
actual: "ShouldCustom",
2457+
wantErr: true,
2458+
},
2459+
{
2460+
name: "redefine builtin assertion error",
2461+
actual: "ShouldEqual",
2462+
wantErr: true,
2463+
},
2464+
}
2465+
2466+
for _, tt := range tests {
2467+
t.Run(tt.name, func(t *testing.T) {
2468+
err := RegisterCustomAssertFunc(tt.actual, assertFunc)
2469+
if (err != nil) != tt.wantErr {
2470+
t.Errorf("RegisterCustomAssertFunc() error = %v, wantErr %v", err, tt.wantErr)
2471+
}
2472+
})
2473+
}
2474+
}

tests/assertions/ShouldCustom.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name: Assertions testsuite
2+
testcases:
3+
- name: test assertion ShouldCustom
4+
steps:
5+
- script: echo foo
6+
assertions:
7+
- result.systemout ShouldCustom bar qux
8+
- result.systemout MustCustom bar qux

tests/lib/ShouldCustom.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
executor: ShouldCustom
2+
input:
3+
test: "{{.argv.argv1}}"
4+
steps:
5+
- script: echo baz
6+
vars:
7+
c:
8+
from: result.systemout
9+
- assertions:
10+
- a ShouldEqual foo
11+
- b ShouldEqual bar
12+
- c ShouldEqual baz
13+
- input.test ShouldEqual qux
14+
- argv.argv1 ShouldEqual qux
15+
- argv.__Len__ ShouldEqual 2

venom.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616

1717
"github.com/confluentinc/bincover"
1818
"github.com/fatih/color"
19+
"github.com/ovh/venom/assertions"
1920
"github.com/pkg/errors"
2021
"github.com/spf13/cast"
2122
)
@@ -280,6 +281,57 @@ func (v *Venom) registerUserExecutors(ctx context.Context) error {
280281
return errors.Wrapf(err, "unable to register user executor %q from file %q", ux.Executor, f)
281282
}
282283
Info(ctx, "User executor %q registered", ux.Executor)
284+
285+
if strings.HasPrefix(ux.Executor, "Should") {
286+
err = v.registerUserAssertFunc(ctx, ux.Executor)
287+
if err != nil {
288+
return errors.Wrapf(err, "unable to register user executor %q from file %q as custom assertion", ux.Executor, f)
289+
}
290+
}
291+
}
292+
293+
return nil
294+
}
295+
296+
func (v *Venom) registerUserAssertFunc(ctx context.Context, name string) error {
297+
_, e, err := v.GetExecutorRunner(ctx, TestStep{"type": name}, H{})
298+
if err != nil {
299+
return errors.Wrapf(err, "unable to load user executor %q", name)
300+
}
301+
302+
err = assertions.RegisterCustomAssertFunc(name, func(actual interface{}, expected ...interface{}) error {
303+
// Prepare a minimal context in which the user executor can be run
304+
// We only need to register the operands in the var set
305+
// as parent contexts will already have been templated
306+
tc := &TestCase{TestCaseInput: TestCaseInput{Name: name}}
307+
tc.TestSuiteVars.Add("a", actual)
308+
if len(expected) > 0 {
309+
tc.TestSuiteVars.Add("b", expected[0])
310+
}
311+
tc.TestSuiteVars.Add("argv", expected)
312+
ts := &TestStepResult{}
313+
314+
_, err := v.RunUserExecutor(ctx, e, tc, ts, TestStep{})
315+
316+
// We reformat any sub-assertions errors to avoid redundant texts
317+
if len(ts.Errors) > 0 {
318+
msg := make([]string, len(ts.Errors))
319+
for _, e := range ts.Errors {
320+
msg = append(msg, fmt.Sprintf(` %d: Sub-assertion %q failed. %s`,
321+
e.StepNumber,
322+
RemoveNotPrintableChar(e.Assertion),
323+
RemoveNotPrintableChar(e.Error.Error()),
324+
))
325+
}
326+
return errors.New(strings.Join(msg, "\n"))
327+
} else if err != nil {
328+
return errors.Wrapf(err, "custom assertion failed during execution")
329+
}
330+
331+
return nil
332+
})
333+
if err != nil {
334+
return errors.Wrapf(err, "unable to register user assertion function %q", name)
283335
}
284336

285337
return nil

0 commit comments

Comments
 (0)