Skip to content

Commit b91d98d

Browse files
authored
Merge pull request #204 from mhuxtable/feature/expect_pings
Implement ExpectPings to watch for Ping attempts on the database
2 parents 36d18c9 + dd0fe2a commit b91d98d

File tree

7 files changed

+329
-5
lines changed

7 files changed

+329
-5
lines changed

expectations.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,32 @@ func (e *queryBasedExpectation) attemptArgMatch(args []namedValue) (err error) {
353353
err = e.argsMatches(args)
354354
return
355355
}
356+
357+
// ExpectedPing is used to manage *sql.DB.Ping expectations.
358+
// Returned by *Sqlmock.ExpectPing.
359+
type ExpectedPing struct {
360+
commonExpectation
361+
delay time.Duration
362+
}
363+
364+
// WillDelayFor allows to specify duration for which it will delay result. May
365+
// be used together with Context.
366+
func (e *ExpectedPing) WillDelayFor(duration time.Duration) *ExpectedPing {
367+
e.delay = duration
368+
return e
369+
}
370+
371+
// WillReturnError allows to set an error for expected database ping
372+
func (e *ExpectedPing) WillReturnError(err error) *ExpectedPing {
373+
e.err = err
374+
return e
375+
}
376+
377+
// String returns string representation
378+
func (e *ExpectedPing) String() string {
379+
msg := "ExpectedPing => expecting database Ping"
380+
if e.err != nil {
381+
msg += fmt.Sprintf(", which should return error: %s", e.err)
382+
}
383+
return msg
384+
}

options.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,19 @@ func QueryMatcherOption(queryMatcher QueryMatcher) func(*sqlmock) error {
2020
return nil
2121
}
2222
}
23+
24+
// MonitorPingsOption determines whether calls to Ping on the driver should be
25+
// observed and mocked.
26+
//
27+
// If true is passed, we will check these calls were expected. Expectations can
28+
// be registered using the ExpectPing() method on the mock.
29+
//
30+
// If false is passed or this option is omitted, calls to Ping will not be
31+
// considered when determining expectations and calls to ExpectPing will have
32+
// no effect.
33+
func MonitorPingsOption(monitorPings bool) func(*sqlmock) error {
34+
return func(s *sqlmock) error {
35+
s.monitorPings = monitorPings
36+
return nil
37+
}
38+
}

sqlmock.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
// for any kind of database action in order to mock
2222
// and test real database behavior.
2323
type Sqlmock interface {
24-
2524
// ExpectClose queues an expectation for this database
2625
// action to be triggered. the *ExpectedClose allows
2726
// to mock database response
@@ -57,6 +56,17 @@ type Sqlmock interface {
5756
// the *ExpectedRollback allows to mock database response
5857
ExpectRollback() *ExpectedRollback
5958

59+
// ExpectPing expected *sql.DB.Ping to be called.
60+
// the *ExpectedPing allows to mock database response
61+
//
62+
// Ping support only exists in the SQL library in Go 1.8 and above.
63+
// ExpectPing in Go <=1.7 will return an ExpectedPing but not register
64+
// any expectations.
65+
//
66+
// You must enable pings using MonitorPingsOption for this to register
67+
// any expectations.
68+
ExpectPing() *ExpectedPing
69+
6070
// MatchExpectationsInOrder gives an option whether to match all
6171
// expectations in the order they were set or not.
6272
//
@@ -83,6 +93,7 @@ type sqlmock struct {
8393
drv *mockDriver
8494
converter driver.ValueConverter
8595
queryMatcher QueryMatcher
96+
monitorPings bool
8697

8798
expected []expectation
8899
}
@@ -104,6 +115,15 @@ func (c *sqlmock) open(options []func(*sqlmock) error) (*sql.DB, Sqlmock, error)
104115
if c.queryMatcher == nil {
105116
c.queryMatcher = QueryMatcherRegexp
106117
}
118+
119+
if c.monitorPings {
120+
// We call Ping on the driver shortly to verify startup assertions by
121+
// driving internal behaviour of the sql standard library. We don't
122+
// want this call to ping to be monitored for expectation purposes so
123+
// temporarily disable.
124+
c.monitorPings = false
125+
defer func() { c.monitorPings = true }()
126+
}
107127
return db, c, db.Ping()
108128
}
109129

sqlmock_before_go18.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// +build !go1.8
2+
3+
package sqlmock
4+
5+
import "log"
6+
7+
func (c *sqlmock) ExpectPing() *ExpectedPing {
8+
log.Println("ExpectPing has no effect on Go 1.7 or below")
9+
return &ExpectedPing{}
10+
}

sqlmock_before_go18_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// +build !go1.8
2+
3+
package sqlmock
4+
5+
import (
6+
"fmt"
7+
"testing"
8+
"time"
9+
)
10+
11+
func TestSqlmockExpectPingHasNoEffect(t *testing.T) {
12+
db, mock, err := New()
13+
if err != nil {
14+
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
15+
}
16+
defer db.Close()
17+
18+
e := mock.ExpectPing()
19+
20+
// Methods on the expectation can be called
21+
e.WillDelayFor(time.Hour).WillReturnError(fmt.Errorf("an error"))
22+
23+
if err = mock.ExpectationsWereMet(); err != nil {
24+
t.Errorf("expected no error to be returned, but got '%s'", err)
25+
}
26+
}

sqlmock_go18.go

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"context"
77
"database/sql/driver"
88
"errors"
9+
"fmt"
10+
"log"
911
"time"
1012
)
1113

@@ -95,11 +97,57 @@ func (c *sqlmock) PrepareContext(ctx context.Context, query string) (driver.Stmt
9597
return nil, err
9698
}
9799

98-
// Implement the "Pinger" interface
99-
// for now we do not have a Ping expectation
100-
// may be something for the future
100+
// Implement the "Pinger" interface - the explicit DB driver ping was only added to database/sql in Go 1.8
101101
func (c *sqlmock) Ping(ctx context.Context) error {
102-
return nil
102+
if !c.monitorPings {
103+
return nil
104+
}
105+
106+
ex, err := c.ping()
107+
if ex != nil {
108+
select {
109+
case <-ctx.Done():
110+
return ErrCancelled
111+
case <-time.After(ex.delay):
112+
}
113+
}
114+
115+
return err
116+
}
117+
118+
func (c *sqlmock) ping() (*ExpectedPing, error) {
119+
var expected *ExpectedPing
120+
var fulfilled int
121+
var ok bool
122+
for _, next := range c.expected {
123+
next.Lock()
124+
if next.fulfilled() {
125+
next.Unlock()
126+
fulfilled++
127+
continue
128+
}
129+
130+
if expected, ok = next.(*ExpectedPing); ok {
131+
break
132+
}
133+
134+
next.Unlock()
135+
if c.ordered {
136+
return nil, fmt.Errorf("call to database Ping, was not expected, next expectation is: %s", next)
137+
}
138+
}
139+
140+
if expected == nil {
141+
msg := "call to database Ping was not expected"
142+
if fulfilled == len(c.expected) {
143+
msg = "all expectations were already fulfilled, " + msg
144+
}
145+
return nil, fmt.Errorf(msg)
146+
}
147+
148+
expected.triggered = true
149+
expected.Unlock()
150+
return expected, expected.err
103151
}
104152

105153
// Implement the "StmtExecContext" interface
@@ -112,4 +160,14 @@ func (stmt *statement) QueryContext(ctx context.Context, args []driver.NamedValu
112160
return stmt.conn.QueryContext(ctx, stmt.query, args)
113161
}
114162

163+
func (c *sqlmock) ExpectPing() *ExpectedPing {
164+
if !c.monitorPings {
165+
log.Println("ExpectPing will have no effect as monitoring pings is disabled. Use MonitorPingsOption to enable.")
166+
return nil
167+
}
168+
e := &ExpectedPing{}
169+
c.expected = append(c.expected, e)
170+
return e
171+
}
172+
115173
// @TODO maybe add ExpectedBegin.WithOptions(driver.TxOptions)

sqlmock_go18_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,3 +474,168 @@ func TestContextExecErrorDelay(t *testing.T) {
474474
t.Errorf("expecting a delay of less than %v before error, actual delay was %v", delay, elapsed)
475475
}
476476
}
477+
478+
// TestMonitorPingsDisabled verifies backwards-compatibility with behaviour of the library in which
479+
// calls to Ping are not mocked out. It verifies this persists when the user does not enable the new
480+
// behaviour.
481+
func TestMonitorPingsDisabled(t *testing.T) {
482+
t.Parallel()
483+
db, mock, err := New()
484+
if err != nil {
485+
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
486+
}
487+
defer db.Close()
488+
489+
// When monitoring of pings is not enabled in the mock, calling Ping should have no effect.
490+
err = db.Ping()
491+
if err != nil {
492+
t.Errorf("monitoring of pings is not enabled so did not expect error from Ping, got '%s'", err)
493+
}
494+
495+
// Calling ExpectPing should also not register any expectations in the mock. The return from
496+
// ExpectPing should be nil.
497+
expectation := mock.ExpectPing()
498+
if expectation != nil {
499+
t.Errorf("expected ExpectPing to return a nil pointer when monitoring of pings is not enabled")
500+
}
501+
502+
err = mock.ExpectationsWereMet()
503+
if err != nil {
504+
t.Errorf("monitoring of pings is not enabled so ExpectPing should not register an expectation, got '%s'", err)
505+
}
506+
}
507+
508+
func TestPingExpectations(t *testing.T) {
509+
t.Parallel()
510+
db, mock, err := New(MonitorPingsOption(true))
511+
if err != nil {
512+
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
513+
}
514+
defer db.Close()
515+
516+
mock.ExpectPing()
517+
if err := db.Ping(); err != nil {
518+
t.Fatal(err)
519+
}
520+
521+
if err := mock.ExpectationsWereMet(); err != nil {
522+
t.Errorf("there were unfulfilled expectations: %s", err)
523+
}
524+
}
525+
526+
func TestPingExpectationsErrorDelay(t *testing.T) {
527+
t.Parallel()
528+
db, mock, err := New(MonitorPingsOption(true))
529+
if err != nil {
530+
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
531+
}
532+
defer db.Close()
533+
534+
var delay time.Duration
535+
delay = 100 * time.Millisecond
536+
mock.ExpectPing().
537+
WillReturnError(errors.New("slow fail")).
538+
WillDelayFor(delay)
539+
540+
start := time.Now()
541+
err = db.Ping()
542+
stop := time.Now()
543+
544+
if err == nil {
545+
t.Errorf("result was not expected, was not expecting nil error")
546+
}
547+
548+
if err.Error() != "slow fail" {
549+
t.Errorf("error '%s' was not expected, was expecting '%s'", err.Error(), "slow fail")
550+
}
551+
552+
elapsed := stop.Sub(start)
553+
if elapsed < delay {
554+
t.Errorf("expecting a delay of %v before error, actual delay was %v", delay, elapsed)
555+
}
556+
557+
mock.ExpectPing().WillReturnError(errors.New("fast fail"))
558+
559+
start = time.Now()
560+
db.Ping()
561+
stop = time.Now()
562+
563+
elapsed = stop.Sub(start)
564+
if elapsed > delay {
565+
t.Errorf("expecting a delay of less than %v before error, actual delay was %v", delay, elapsed)
566+
}
567+
}
568+
569+
func TestPingExpectationsMissingPing(t *testing.T) {
570+
t.Parallel()
571+
db, mock, err := New(MonitorPingsOption(true))
572+
if err != nil {
573+
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
574+
}
575+
defer db.Close()
576+
577+
mock.ExpectPing()
578+
579+
if err = mock.ExpectationsWereMet(); err == nil {
580+
t.Fatalf("was expecting an error, but there wasn't one")
581+
}
582+
}
583+
584+
func TestPingExpectationsUnexpectedPing(t *testing.T) {
585+
t.Parallel()
586+
db, _, err := New(MonitorPingsOption(true))
587+
if err != nil {
588+
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
589+
}
590+
defer db.Close()
591+
592+
if err = db.Ping(); err == nil {
593+
t.Fatalf("was expecting an error, but there wasn't any")
594+
}
595+
}
596+
597+
func TestPingOrderedWrongOrder(t *testing.T) {
598+
t.Parallel()
599+
db, mock, err := New(MonitorPingsOption(true))
600+
if err != nil {
601+
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
602+
}
603+
defer db.Close()
604+
605+
mock.ExpectBegin()
606+
mock.ExpectPing()
607+
mock.MatchExpectationsInOrder(true)
608+
609+
if err = db.Ping(); err == nil {
610+
t.Fatalf("was expecting an error, but there wasn't any")
611+
}
612+
}
613+
614+
func TestPingExpectationsContextTimeout(t *testing.T) {
615+
t.Parallel()
616+
db, mock, err := New(MonitorPingsOption(true))
617+
if err != nil {
618+
t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
619+
}
620+
defer db.Close()
621+
622+
mock.ExpectPing().WillDelayFor(time.Hour)
623+
624+
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
625+
defer cancel()
626+
627+
doneCh := make(chan struct{})
628+
go func() {
629+
err = db.PingContext(ctx)
630+
close(doneCh)
631+
}()
632+
633+
select {
634+
case <-doneCh:
635+
if err != ErrCancelled {
636+
t.Errorf("expected error '%s' to be returned from Ping, but got '%s'", ErrCancelled, err)
637+
}
638+
case <-time.After(time.Second):
639+
t.Errorf("expected Ping to return after context timeout, but it did not in a timely fashion")
640+
}
641+
}

0 commit comments

Comments
 (0)