Skip to content

Commit 75f62a6

Browse files
Added sqlmock-based unit tests for every mysql store file (#378)
## Why? Added sqlmock-based unit tests for every mysql store file in both `stovepipe/extension/storage/mysql/` and `submitqueue/extension/storage/mysql/`. ## What? Added one `_test.go` per store file (17 new files, no production code changed): - **stovepipe**: `storage_test.go`, `build_store_test.go`, `queue_store_test.go`, `request_store_test.go` (+ `isDuplicateEntry`), `request_uri_store_test.go`. - **submitqueue**: `storage_test.go`, `build_store_test.go`, `batch_store_test.go`, `batch_dependent_store_test.go`, `change_store_test.go`, `request_log_store_test.go`, `request_store_test.go`, `request_summary_store_test.go` (+ `normalizeChangeURIs`/`normalizeMetadata`), `request_queue_summary_store_test.go`, `request_uri_store_test.go`, `speculation_path_build_store_test.go`, `speculation_tree_store_test.go`. ## Test Plan `make build` & `make test` - all clean ## Issue <!-- Link the issue here. - Use 'Closes #123' if this is the final fix. - Use 'Part of #123' or just '#123' if the feature is still in progress. --> Co-authored-by: Preetam Dwivedi <behinddwalls@gmail.com>
1 parent 4ddcdb9 commit 75f62a6

19 files changed

Lines changed: 3881 additions & 2 deletions

stovepipe/extension/storage/mysql/BUILD.bazel

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
load("@rules_go//go:def.bzl", "go_library")
1+
load("@rules_go//go:def.bzl", "go_library", "go_test")
22

33
go_library(
44
name = "go_default_library",
@@ -19,3 +19,24 @@ go_library(
1919
"@com_github_uber_go_tally//:go_default_library",
2020
],
2121
)
22+
23+
go_test(
24+
name = "go_default_test",
25+
srcs = [
26+
"build_store_test.go",
27+
"queue_store_test.go",
28+
"request_store_test.go",
29+
"request_uri_store_test.go",
30+
"storage_test.go",
31+
],
32+
embed = [":go_default_library"],
33+
deps = [
34+
"//stovepipe/entity:go_default_library",
35+
"//stovepipe/extension/storage:go_default_library",
36+
"@com_github_data_dog_go_sqlmock//:go_default_library",
37+
"@com_github_go_sql_driver_mysql//:go_default_library",
38+
"@com_github_stretchr_testify//assert:go_default_library",
39+
"@com_github_stretchr_testify//require:go_default_library",
40+
"@com_github_uber_go_tally//:go_default_library",
41+
],
42+
)
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package mysql
16+
17+
import (
18+
"context"
19+
"database/sql"
20+
"fmt"
21+
"testing"
22+
23+
"github.com/DATA-DOG/go-sqlmock"
24+
"github.com/go-sql-driver/mysql"
25+
"github.com/stretchr/testify/assert"
26+
"github.com/stretchr/testify/require"
27+
28+
"github.com/uber/submitqueue/stovepipe/entity"
29+
"github.com/uber/submitqueue/stovepipe/extension/storage"
30+
)
31+
32+
func setupBuildStoreTest(t *testing.T) (*sql.DB, sqlmock.Sqlmock, storage.BuildStore) {
33+
t.Helper()
34+
35+
db, mock, err := sqlmock.New()
36+
require.NoError(t, err)
37+
38+
store := NewBuildStore(db, testMetrics())
39+
40+
return db, mock, store
41+
}
42+
43+
func TestBuildStore_Create(t *testing.T) {
44+
build := entity.Build{
45+
ID: "bk-1001",
46+
RequestID: "request/monorepo/main/1",
47+
Status: entity.BuildStatusAccepted,
48+
Version: 1,
49+
}
50+
51+
tests := []struct {
52+
name string
53+
setup func(mock sqlmock.Sqlmock)
54+
wantErr bool
55+
wantErrIs error
56+
}{
57+
{
58+
name: "success",
59+
setup: func(mock sqlmock.Sqlmock) {
60+
mock.ExpectExec("INSERT INTO build").
61+
WithArgs(build.ID, build.RequestID, build.Status, build.Version).
62+
WillReturnResult(sqlmock.NewResult(0, 1))
63+
},
64+
},
65+
{
66+
name: "duplicate id returns ErrAlreadyExists",
67+
setup: func(mock sqlmock.Sqlmock) {
68+
mock.ExpectExec("INSERT INTO build").
69+
WithArgs(build.ID, build.RequestID, build.Status, build.Version).
70+
WillReturnError(&mysql.MySQLError{Number: mysqlErrDuplicateEntry})
71+
},
72+
wantErr: true,
73+
wantErrIs: storage.ErrAlreadyExists,
74+
},
75+
{
76+
name: "other exec error",
77+
setup: func(mock sqlmock.Sqlmock) {
78+
mock.ExpectExec("INSERT INTO build").
79+
WithArgs(build.ID, build.RequestID, build.Status, build.Version).
80+
WillReturnError(fmt.Errorf("connection reset"))
81+
},
82+
wantErr: true,
83+
},
84+
}
85+
86+
for _, tt := range tests {
87+
t.Run(tt.name, func(t *testing.T) {
88+
db, mock, store := setupBuildStoreTest(t)
89+
defer db.Close()
90+
91+
tt.setup(mock)
92+
93+
err := store.Create(context.Background(), build)
94+
if tt.wantErr {
95+
require.Error(t, err)
96+
if tt.wantErrIs != nil {
97+
assert.ErrorIs(t, err, tt.wantErrIs)
98+
}
99+
} else {
100+
require.NoError(t, err)
101+
}
102+
require.NoError(t, mock.ExpectationsWereMet())
103+
})
104+
}
105+
}
106+
107+
func TestBuildStore_Get(t *testing.T) {
108+
want := entity.Build{
109+
ID: "bk-1001",
110+
RequestID: "request/monorepo/main/1",
111+
Status: entity.BuildStatusRunning,
112+
Version: 2,
113+
}
114+
115+
tests := []struct {
116+
name string
117+
id string
118+
setup func(mock sqlmock.Sqlmock)
119+
want entity.Build
120+
wantErr bool
121+
wantErrIs error
122+
}{
123+
{
124+
name: "found",
125+
id: want.ID,
126+
setup: func(mock sqlmock.Sqlmock) {
127+
rows := sqlmock.NewRows([]string{"id", "request_id", "status", "version"}).
128+
AddRow(want.ID, want.RequestID, string(want.Status), want.Version)
129+
mock.ExpectQuery("SELECT id, request_id, status, version").
130+
WithArgs(want.ID).
131+
WillReturnRows(rows)
132+
},
133+
want: want,
134+
},
135+
{
136+
name: "not found",
137+
id: "missing",
138+
setup: func(mock sqlmock.Sqlmock) {
139+
mock.ExpectQuery("SELECT id, request_id, status, version").
140+
WithArgs("missing").
141+
WillReturnError(sql.ErrNoRows)
142+
},
143+
wantErr: true,
144+
wantErrIs: storage.ErrNotFound,
145+
},
146+
{
147+
name: "query error",
148+
id: "bad",
149+
setup: func(mock sqlmock.Sqlmock) {
150+
mock.ExpectQuery("SELECT id, request_id, status, version").
151+
WithArgs("bad").
152+
WillReturnError(fmt.Errorf("connection reset"))
153+
},
154+
wantErr: true,
155+
},
156+
}
157+
158+
for _, tt := range tests {
159+
t.Run(tt.name, func(t *testing.T) {
160+
db, mock, store := setupBuildStoreTest(t)
161+
defer db.Close()
162+
163+
tt.setup(mock)
164+
165+
got, err := store.Get(context.Background(), tt.id)
166+
if tt.wantErr {
167+
require.Error(t, err)
168+
if tt.wantErrIs != nil {
169+
assert.ErrorIs(t, err, tt.wantErrIs)
170+
}
171+
} else {
172+
require.NoError(t, err)
173+
assert.Equal(t, tt.want, got)
174+
}
175+
require.NoError(t, mock.ExpectationsWereMet())
176+
})
177+
}
178+
}
179+
180+
func TestBuildStore_Update(t *testing.T) {
181+
build := entity.Build{ID: "bk-1001", Status: entity.BuildStatusRunning}
182+
const oldVersion, newVersion = int32(1), int32(2)
183+
184+
tests := []struct {
185+
name string
186+
setup func(mock sqlmock.Sqlmock)
187+
wantErr bool
188+
wantErrIs error
189+
}{
190+
{
191+
name: "success",
192+
setup: func(mock sqlmock.Sqlmock) {
193+
mock.ExpectExec("UPDATE build").
194+
WithArgs(build.Status, newVersion, build.ID, oldVersion).
195+
WillReturnResult(sqlmock.NewResult(0, 1))
196+
},
197+
},
198+
{
199+
name: "version mismatch",
200+
setup: func(mock sqlmock.Sqlmock) {
201+
mock.ExpectExec("UPDATE build").
202+
WithArgs(build.Status, newVersion, build.ID, oldVersion).
203+
WillReturnResult(sqlmock.NewResult(0, 0))
204+
},
205+
wantErr: true,
206+
wantErrIs: storage.ErrVersionMismatch,
207+
},
208+
{
209+
name: "exec error",
210+
setup: func(mock sqlmock.Sqlmock) {
211+
mock.ExpectExec("UPDATE build").
212+
WithArgs(build.Status, newVersion, build.ID, oldVersion).
213+
WillReturnError(fmt.Errorf("connection reset"))
214+
},
215+
wantErr: true,
216+
},
217+
{
218+
name: "rows affected error",
219+
setup: func(mock sqlmock.Sqlmock) {
220+
mock.ExpectExec("UPDATE build").
221+
WithArgs(build.Status, newVersion, build.ID, oldVersion).
222+
WillReturnResult(sqlmock.NewErrorResult(fmt.Errorf("driver error")))
223+
},
224+
wantErr: true,
225+
},
226+
}
227+
228+
for _, tt := range tests {
229+
t.Run(tt.name, func(t *testing.T) {
230+
db, mock, store := setupBuildStoreTest(t)
231+
defer db.Close()
232+
233+
tt.setup(mock)
234+
235+
err := store.Update(context.Background(), build, oldVersion, newVersion)
236+
if tt.wantErr {
237+
require.Error(t, err)
238+
if tt.wantErrIs != nil {
239+
assert.ErrorIs(t, err, tt.wantErrIs)
240+
}
241+
} else {
242+
require.NoError(t, err)
243+
}
244+
require.NoError(t, mock.ExpectationsWereMet())
245+
})
246+
}
247+
}

0 commit comments

Comments
 (0)