I've actually used your first approach with quite some success, but in a slightly different ways that I think would solve some of your problems:
Keep the entire schema and scripts for creating it in source control so that anyone can create the current database schema after a check out. In addition, keep sample data in data files that get loaded by part of the build process. As you discover data that causes errors, add it to your sample data to check that errors don't re-emerge.
Use a continuous integration server to build the database schema, load the sample data, and run tests. This is how we keep our test database in sync (rebuilding it at every test run). Though this requires that the CI server have access and ownership of its own dedicated database instance, I say that having our db schema built 3 times a day has dramatically helped find errors that probably would not have been found till just before delivery (if not later). I can't say that I rebuild the schema before every commit. Does anybody? With this approach you won't have to (well maybe we should, but its not a big deal if someone forgets).
For my group, user input is done at the application level (not db) so this is tested via standard unit tests.
Loading Production Database Copy:
This was the approach that was used at my last job. It was a huge pain cause of a couple of issues:
- The copy would get out of date from the production version
- Changes would be made to the copy's schema and wouldn't get propagated to the production systems. At this point we'd have diverging schemas. Not fun.
Mocking Database Server:
We also do this at my current job. After every commit we execute unit tests against the application code that have mock db accessors injected. Then three times a day we execute the full db build described above. I definitely recommend both approaches.
A tag for a field allows you to attach meta-information to the field which can be acquired using reflection. Usually it is used to provide transformation info on how a struct field is encoded to or decoded from another format (or stored/retrieved from a database), but you can use it to store whatever meta-info you want to, either intended for another package or for your own use.
As mentioned in the documentation of reflect.StructTag
, by convention the value of a tag string is a space-separated list of key:"value"
pairs, for example:
type User struct {
Name string `json:"name" xml:"name"`
}
The key
usually denotes the package that the subsequent "value"
is for, for example json
keys are processed/used by the encoding/json
package.
If multiple information is to be passed in the "value"
, usually it is specified by separating it with a comma (','
), e.g.
Name string `json:"name,omitempty" xml:"name"`
Usually a dash value ('-'
) for the "value"
means to exclude the field from the process (e.g. in case of json
it means not to marshal or unmarshal that field).
Example of accessing your custom tags using reflection
We can use reflection (reflect
package) to access the tag values of struct fields. Basically we need to acquire the Type
of our struct, and then we can query fields e.g. with Type.Field(i int)
or Type.FieldByName(name string)
. These methods return a value of StructField
which describes / represents a struct field; and StructField.Tag
is a value of type [StructTag
] 6 which describes / represents a tag value.
Previously we talked about "convention". This convention means that if you follow it, you may use the StructTag.Get(key string)
method which parses the value of a tag and returns you the "value"
of the key
you specify. The convention is implemented / built into this Get()
method. If you don't follow the convention, Get()
will not be able to parse key:"value"
pairs and find what you're looking for. That's also not a problem, but then you need to implement your own parsing logic.
Also there is StructTag.Lookup()
(was added in Go 1.7) which is "like Get()
but distinguishes the tag not containing the given key from the tag associating an empty string with the given key".
So let's see a simple example:
type User struct {
Name string `mytag:"MyName"`
Email string `mytag:"MyEmail"`
}
u := User{"Bob", "bob@mycompany.com"}
t := reflect.TypeOf(u)
for _, fieldName := range []string{"Name", "Email"} {
field, found := t.FieldByName(fieldName)
if !found {
continue
}
fmt.Printf("\nField: User.%s\n", fieldName)
fmt.Printf("\tWhole tag value : %q\n", field.Tag)
fmt.Printf("\tValue of 'mytag': %q\n", field.Tag.Get("mytag"))
}
Output (try it on the Go Playground):
Field: User.Name
Whole tag value : "mytag:\"MyName\""
Value of 'mytag': "MyName"
Field: User.Email
Whole tag value : "mytag:\"MyEmail\""
Value of 'mytag': "MyEmail"
GopherCon 2015 had a presentation about struct tags called:
The Many Faces of Struct Tags (slide) (and a video)
Here is a list of commonly used tag keys:
json
- used by the encoding/json
package, detailed at json.Marshal()
xml
- used by the encoding/xml
package, detailed at xml.Marshal()
bson
- used by gobson, detailed at bson.Marshal()
; also by the mongo-go driver, detailed at bson package doc
protobuf
- used by github.com/golang/protobuf/proto
, detailed in the package doc
yaml
- used by the gopkg.in/yaml.v2
package, detailed at yaml.Marshal()
db
- used by the github.com/jmoiron/sqlx
package; also used by github.com/go-gorp/gorp
package
orm
- used by the github.com/astaxie/beego/orm
package, detailed at Models – Beego ORM
gorm
- used by gorm.io/gorm
, examples can be found in their docs
valid
- used by the github.com/asaskevich/govalidator
package, examples can be found in the project page
datastore
- used by appengine/datastore
(Google App Engine platform, Datastore service), detailed at Properties
schema
- used by github.com/gorilla/schema
to fill a struct
with HTML form values, detailed in the package doc
asn
- used by the encoding/asn1
package, detailed at asn1.Marshal()
and asn1.Unmarshal()
csv
- used by the github.com/gocarina/gocsv
package
env
- used by the github.com/caarlos0/env
package
Best Answer
The fundamental difference between the three strategies you've listed is whether or not the test code is in the same package as the code under test. The decision to use
package myfunc
orpackage myfunc_test
in the test file depends on whether you want to perform white-box or black-box testing.There's nothing wrong with using both methods in a project. For instance, you could have
myfunc_whitebox_test.go
andmyfunx_blackbox_test.go
.Test Code Package Comparison
package myfunc_test
, which will ensure you're only using the exported identifiers.package myfunc
so that you have access to the non-exported identifiers. Good for unit tests that require access to non-exported variables, functions, and methods.Comparison of Strategies Listed in Question
myfunc_test.go
usespackage myfunc
— In this case the test code inmyfunc_test.go
will be in the same package as the code being tested inmyfunc.go
, which ismyfunc
in this example.myfunc_test.go
usespackage myfunc_test
— In this case the test code inmyfunc_test.go
"will be compiled as a separate package, and then linked and run with the main test binary." [Source: Lines 58–59 in the test.go source code]myfunc_test.go
usespackage myfunc_test
but importsmyfunc
using the dot notation — This is a variant of Strategy 2, but uses the dot notation to importmyfunc
.