Skip to content

Commit b22065b

Browse files
davidhartingclaude
authored andcommitted
Add testassert.ContainsInOrder assertion helper (#457)
A small package complementing testify: ContainsInOrder asserts that several substrings appear in a string as an ordered subsequence (each after the previous), mirroring Laravel's assertSeeInOrder. Useful for asserting the layout of rendered text output. Takes the same minimal interface as testify's assertions so a *testing.T just works. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> GitOrigin-RevId: 031e0d918edc1330fc85dbbb04bc8a0ddbed7432
1 parent 013f31d commit b22065b

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

internal/testassert/testassert.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Package testassert provides additional assertion helpers that
2+
// stretchr/testify does not cover.
3+
package testassert
4+
5+
import (
6+
"fmt"
7+
"strings"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
// ContainsInOrder asserts that every substr appears in s AND that each appears
13+
// after the previous one (an ordered subsequence).
14+
// Handy for asserting the layout of rendered text output.
15+
// Each substr is matched at its first occurrence at or after the end
16+
// of the previous match. Returns true when the assertion passes.
17+
//
18+
// With no substrings it passes, matching the convention of
19+
// strings.Contains(s, "") and testify's assert.Subset with an empty subset.
20+
//
21+
// It accepts the same minimal interface as testify's own assertions (a
22+
// *testing.T satisfies it), and reports the failing position through
23+
// t.Helper()/t.Errorf so failures point at the caller.
24+
func ContainsInOrder(t assert.TestingT, s string, substrs ...string) bool {
25+
if h, ok := t.(interface{ Helper() }); ok {
26+
h.Helper()
27+
}
28+
pos := 0
29+
for _, sub := range substrs {
30+
i := strings.Index(s[pos:], sub)
31+
if i < 0 {
32+
return assert.Fail(t, fmt.Sprintf("substring %q not found in expected order", sub),
33+
fmt.Sprintf("expected it after offset %d in:\n%s", pos, s))
34+
}
35+
pos += i + len(sub)
36+
}
37+
return true
38+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package testassert_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/render-oss/cli/internal/testassert"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestContainsInOrder(t *testing.T) {
11+
const s = "alpha bravo charlie"
12+
13+
t.Run("substrings in order pass", func(t *testing.T) {
14+
assert.True(t, testassert.ContainsInOrder(t, s, "alpha", "charlie"))
15+
})
16+
17+
t.Run("single substring present", func(t *testing.T) {
18+
assert.True(t, testassert.ContainsInOrder(t, s, "bravo"))
19+
})
20+
21+
// Vacuously true with no substrings, like strings.Contains(s, "") — asserting
22+
// nothing can't be violated, and it keeps dynamically-built arg lists safe.
23+
t.Run("no substrings always passes", func(t *testing.T) {
24+
assert.True(t, testassert.ContainsInOrder(t, s))
25+
})
26+
27+
t.Run("out-of-order fails", func(t *testing.T) {
28+
// "alpha" appears before "charlie", so requesting them reversed fails.
29+
spy := &spyT{}
30+
assert.False(t, testassert.ContainsInOrder(spy, s, "charlie", "alpha"))
31+
assert.True(t, spy.failed, "expected out-of-order assertion to fail")
32+
})
33+
34+
t.Run("missing substring fails", func(t *testing.T) {
35+
spy := &spyT{}
36+
assert.False(t, testassert.ContainsInOrder(spy, s, "delta"))
37+
assert.True(t, spy.failed, "expected missing substring to fail")
38+
})
39+
40+
t.Run("repeated substring needs two occurrences", func(t *testing.T) {
41+
// Only one "alpha", so requiring it twice must fail.
42+
spy := &spyT{}
43+
assert.False(t, testassert.ContainsInOrder(spy, s, "alpha", "alpha"))
44+
assert.True(t, spy.failed, "expected second occurrence requirement to fail")
45+
})
46+
}
47+
48+
// spyT implements assert.TestingT (just Errorf) so we can observe failures on
49+
// the negative cases without failing the real test.
50+
type spyT struct{ failed bool }
51+
52+
func (s *spyT) Errorf(string, ...any) { s.failed = true }

0 commit comments

Comments
 (0)