From fd000136eb5b1043974b6fd9c634f515f6c3574b Mon Sep 17 00:00:00 2001 From: Arsham Shirvani Date: Tue, 2 Jul 2019 23:01:55 +0100 Subject: [PATCH] Add ValueRecorder and OkValue --- .gitignore | 1 + LICENSE | 190 ++++++++++++++++++++++++++++++++++ README.md | 79 +++++++++++++- dbtesting.go | 84 +++++++++++++++ dbtesting_test.go | 255 ++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 + go.sum | 2 + 7 files changed, 615 insertions(+), 1 deletion(-) create mode 100644 LICENSE create mode 100644 dbtesting.go create mode 100644 dbtesting_test.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/.gitignore b/.gitignore index f1c181e..87b0c13 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out +vendor/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f18d8d3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written token sent + to the Licensor or its representatives, including but not limited to + token on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding token that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2019 Arsham Shirvani + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 1e1de88..9b26806 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,79 @@ # dbtesting -Utility for using with go-sqlmock library + +Utility for using with go-sqlmock library. + +This library can be used in the [go-sqlmock][go-sqlmock] test cases for cases +that values are random but it is important to check the values passed in +queries. + +## ValueRecorder + +If you generate a UUID and use it in multiple queries and you want to make sure +the queries are passed with correct IDs. For instance if in your code you have: + +```go +import "database/sql" + +// ... + +// assume num has been generated randomly +num := 666 +_, err := tx.ExecContext(ctx, "INSERT INTO life (value) VALUE ($1)", num) +// error check +_, err := tx.ExecContext(ctx, "INSERT INTO reality (value) VALUE ($1)", num) +// error check +_, err := tx.ExecContext(ctx, "INSERT INTO everywhere (value) VALUE ($1)", num) +// error check +``` + +Your tests can be checked easily like this: +```go +import ( + "github.com/arsham/dbtesting" + "github.com/DATA-DOG/go-sqlmock" + // ... +) + +rec := dbtesting.NewValueRecorder() +mock.ExpectExec("INSERT INTO life .+"). + WithArgs(rec.Record("truth")). + WillReturnResult(sqlmock.NewResult(1, 1)) +mock.ExpectExec("INSERT INTO reality .+"). + WithArgs(rec.For("truth")). + WillReturnResult(sqlmock.NewResult(1, 1)) +mock.ExpectExec("INSERT INTO everywhere .+"). + WithArgs(rec.For("truth")). + WillReturnResult(sqlmock.NewResult(1, 1)) +``` + +## OkValue + +When you are only interested in checking some arguments passed to the Exec/Query +functions and you don't want to check everything (maybe because thy are not +relevant to the current test), you can use the `OkValue`. + +```go +import ( + "github.com/arsham/dbtesting" + "github.com/DATA-DOG/go-sqlmock" + // ... +) + +mock.ExpectExec("INSERT INTO life .+"). + WithArgs( + dbtesting.OkValue, + dbtesting.OkValue, + dbtesting.OkValue, + "import value" + dbtesting.OkValue, + dbtesting.OkValue, + dbtesting.OkValue, + ) +``` + +## LICENSE + +Use of this source code is governed by the Apache 2.0 license. License can be +found in the [LICENSE](./LICENSE) file. + +[go-sqlmock]: github.com/DATA-DOG/go-sqlmock diff --git a/dbtesting.go b/dbtesting.go new file mode 100644 index 0000000..1a86b77 --- /dev/null +++ b/dbtesting.go @@ -0,0 +1,84 @@ +package dbtesting + +import ( + "database/sql/driver" + "reflect" + + "github.com/DATA-DOG/go-sqlmock" +) + +// OkValue is used for sqlmock package for when the checks should always return +// true. +var OkValue = okValue{} + +type okValue struct{} + +// Match always returns true. +func (okValue) Match(driver.Value) bool { return true } + +// ValueRecorder records the values when they are seen and compares them when +// they are asked. You can create a new ValueRecorder with NewValueRecorder +// function. Values should have one Record call and zero or more For calls. +type ValueRecorder interface { + // Record records the value of the value the first time it sees it. It panics + // if the value is already been recorded. + Record(name string) sqlmock.Argument + // For reuses the value in the query. It panics if the value is not been + // recorded. + For(name string) sqlmock.Argument + // Value returns the recorded value of the item. It panics if the value is not + // been recorded. + Value(name string) interface{} +} + +// NewValueRecorder returns a fresh ValueRecorder instance. +func NewValueRecorder() ValueRecorder { + return make(valueRecorder) +} + +type value struct { + val interface{} + valid bool +} + +func (v *value) Match(val driver.Value) bool { + if !v.valid { + v.val = val + v.valid = true + return true + } + return reflect.DeepEqual(val, v.val) +} + +type valueRecorder map[string]*value + +// Record records the value of the value the first time it sees it. It panics if +// the value is already been recorded. +func (v valueRecorder) Record(s string) sqlmock.Argument { + _, ok := v[s] + if ok { + panic(s + " recorded twice") + } + v[s] = &value{} + return v[s] +} + +// For reuses the value in the query. It panics if the value is not been +// recorded. +func (v valueRecorder) For(s string) sqlmock.Argument { + id, ok := v[s] + if !ok || id == nil { + panic(s + " not recorded yet") + } + return id +} + +// Value returns the recorded value of the item. It panics if the value is not +// been recorded. +func (v valueRecorder) Value(s string) interface{} { + id, ok := v[s] + if !ok || id == nil { + panic(s + " not recorded yet") + } + return id.val +} diff --git a/dbtesting_test.go b/dbtesting_test.go new file mode 100644 index 0000000..1bf483d --- /dev/null +++ b/dbtesting_test.go @@ -0,0 +1,255 @@ +package dbtesting_test + +import ( + "fmt" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/arsham/dbtesting" +) + +func TestOkValue(t *testing.T) { + t.Parallel() + tcs := map[string]interface{}{ + "nil": nil, + "int": 666, + "float": 66.6, + "string": "satan", + "byte slice": []byte("devil"), + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("got %v, want nil", err) + } + defer db.Close() + defer func() { + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }() + mock.ExpectExec("INSERT INTO life .+"). + WithArgs(dbtesting.OkValue). + WillReturnResult(sqlmock.NewResult(1, 1)) + _, err = db.Exec("INSERT INTO life (name) VALUE ($1)", tc) + if err != nil { + t.Errorf("got %v, want nil", err) + } + }) + } +} + +func ExampleOkValue() { + db, mock, err := sqlmock.New() + if err != nil { + panic(err) + } + defer db.Close() + defer func() { + if err := mock.ExpectationsWereMet(); err != nil { + fmt.Printf("there were unfulfilled expectations: %s", err) + } + }() + mock.ExpectExec("INSERT INTO life .+"). + WithArgs(dbtesting.OkValue). + WillReturnResult(sqlmock.NewResult(1, 1)) + _, err = db.Exec("INSERT INTO life (name) VALUE ($1)", 666) + if err != nil { + panic(err) + } + + // Output: +} + +func TestValueRecorder(t *testing.T) { + t.Run("Record", testValueRecorderRecord) + t.Run("RecordPanic", testValueRecorderRecordPanic) + t.Run("For", testValueRecorderFor) + t.Run("ForPanic", testValueRecorderForPanic) + t.Run("Value", testValueRecorderValue) + t.Run("ValuePanic", testValueRecorderValuePanic) +} + +func testValueRecorderRecord(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("got %v, want nil", err) + } + defer db.Close() + defer func() { + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }() + defer func() { + if e := recover(); e != nil { + t.Errorf("didn't expect to panic: %v", e) + } + }() + rec := dbtesting.NewValueRecorder() + mock.ExpectExec(".+"). + WithArgs(rec.Record("satan")). + WillReturnResult(sqlmock.NewResult(1, 1)) + _, err = db.Exec("query", float64(66.6)) + if err != nil { + t.Fatalf("got %v, want nil", err) + } + got := rec.Value("satan") + if v, ok := got.(float64); !ok || v != 66.6 { + t.Errorf("%+v: got %f, want %f", got, v, 66.6) + } +} + +func testValueRecorderRecordPanic(t *testing.T) { + t.Parallel() + defer func() { + paniced := false + if e := recover(); e != nil { + paniced = true + } + if !paniced { + t.Error("did not panic") + } + }() + rec := dbtesting.NewValueRecorder() + rec.Record("god") + rec.Record("god") +} + +func testValueRecorderFor(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("got %v, want nil", err) + } + defer db.Close() + defer func() { + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }() + defer func() { + if e := recover(); e != nil { + t.Errorf("didn't expect to panic: %v", e) + } + }() + rec := dbtesting.NewValueRecorder() + mock.ExpectExec("query1"). + WithArgs(rec.Record("satan")). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec("query2"). + WithArgs(rec.For("satan")). + WillReturnResult(sqlmock.NewResult(1, 1)) + _, err = db.Exec("query1", float64(66.6)) + if err != nil { + t.Fatalf("got %v, want nil", err) + } + _, err = db.Exec("query2", float64(66.6)) + if err != nil { + t.Fatalf("got %v, want nil", err) + } +} + +func testValueRecorderForPanic(t *testing.T) { + t.Parallel() + defer func() { + paniced := false + if e := recover(); e != nil { + paniced = true + } + if !paniced { + t.Error("did not panic") + } + }() + rec := dbtesting.NewValueRecorder() + rec.For("god") +} + +func testValueRecorderValue(t *testing.T) { + t.Parallel() + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("got %v, want nil", err) + } + defer db.Close() + defer func() { + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }() + defer func() { + if e := recover(); e != nil { + t.Errorf("didn't expect to panic: %v", e) + } + }() + val := float64(66.6) + rec := dbtesting.NewValueRecorder() + mock.ExpectExec("query"). + WithArgs(rec.Record("satan")). + WillReturnResult(sqlmock.NewResult(1, 1)) + _, err = db.Exec("query", val) + if err != nil { + t.Fatalf("got %v, want nil", err) + } + got := rec.Value("satan").(float64) + if got != val { + t.Errorf("got %f, want %f", got, val) + } +} + +func testValueRecorderValuePanic(t *testing.T) { + t.Parallel() + defer func() { + paniced := false + if e := recover(); e != nil { + paniced = true + } + if !paniced { + t.Error("did not panic") + } + }() + rec := dbtesting.NewValueRecorder() + rec.Value("god") +} + +func ExampleValueRecorder() { + db, mock, err := sqlmock.New() + if err != nil { + panic(err) + } + defer db.Close() + defer func() { + if err := mock.ExpectationsWereMet(); err != nil { + fmt.Printf("there were unfulfilled expectations: %s", err) + } + }() + rec := dbtesting.NewValueRecorder() + mock.ExpectExec("INSERT INTO life .+"). + WithArgs(rec.Record("truth")). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec("INSERT INTO reality .+"). + WithArgs(rec.For("truth")). + WillReturnResult(sqlmock.NewResult(1, 1)) + + // pretend the following query happens in another package and the argument is + // totally random. + _, err = db.Exec("INSERT INTO life (name) VALUE ($1)", 666) + if err != nil { + panic(err) + } + + // say we don't have access to the value and we don't know what value would be + // passed, but it is important the value is the same as the logic has to pass. + + _, err = db.Exec("INSERT INTO reality (name) VALUE ($1)", 666) + if err != nil { + panic(err) + } + + fmt.Printf("got recorded value: %d", rec.Value("truth")) + + // Output: + // got recorded value: 666 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..648963d --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/arsham/dbtesting + +go 1.12 + +require github.com/DATA-DOG/go-sqlmock v1.3.3 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e787bfc --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= +github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=