Imagine we build REST API server using Go with following specs :
- Feature
Bucket Create
with endpointPOST /buckets
and want to set our Bucket entitycreated_at
datetime value from our code as the request arrive usingtime.Now()
. - Feature
Bucket List
with endpointGET /buckets
and will return all bucket whichcreated_at
datetime value is betweentime.Now
and (time.Now
- 24 hours) as the request arrive.
And then there is Alice, a QA tester who want to test our API server functionality.
Alice want to simulate Bucket List
and Bucket Create
feature as a real user, and want to simulate bucket creation as following :
- Bucket A created two days ago
- Bucket B created twelve hour ago
- Bucket C created right now
- Bucket D created tomorrow
All bucket creation must be created using Bucket Create
feature. And then Bucket List
must return only B and C bucket according to the feature spec.
But when Alice test Create and List, she will get all bucket A,B,C,D because on API server created_at
automatically use time.Now()
value which is current server time.
If you are developer who wrote that backend code, what would you do to help Alice test your API server as real user with those specs?
Solution 1
Add a middleware which will give Alice freedom to set API server time value.
That middleware must be able to Monkey Patch time.Now
,
so any function/method which call time.Now()
will use new value and then Unpatch it after actual handler process completed.
With help of monkey package, we can create simple middleware which will read query param value and set time.Now
value like this :
import (
"net/http"
"strconv"
"time"
"github.com/bouk/monkey"
)
// TimePatch patch time.Now to certain unix timestamp based on submitted query parameter value.
func TimePatch(patchParamName string) func(next http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
patchQuery := r.URL.Query().Get(patchParamName)
ts, err := strconv.ParseInt(patchQuery, 0, 64)
if err != nil {
h.ServeHTTP(w, r)
return
}
// patch time!
monkey.Patch(time.Now, func() time.Time {
return time.Unix(ts, 0)
})
// do whatever with time.Now() from your handler
h.ServeHTTP(w, r)
// restore patched time!
monkey.Unpatch(time.Now)
}
return http.HandlerFunc(fn)
}
}
Using Chi for router and middleware chain (you can use whatever router you want), we can use that middleware like this :
import (
"net/http"
"time"
"github.com/go-chi/chi"
)
func imaginary() string {
currentTime := time.Now()
return currentTime.UTC().Format("Mon Jan 02 15:04:05 MST 2006")
}
func main() {
r := chi.NewRouter()
// if enable timepatch (testing only)
r.Use(TimePatch("__timepatch-ts__")) // __timepatch-ts__ can be configured
// endif
// sample to get server current time based on time.Now
r.Get("/now", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(imaginary()))
})
// POST /buckets (handler will use modified time.Now)
// GET /buckets (handler will use modified time.Now)
http.ListenAndServe(":8080", r)
}
After we add TimePatch
middleware, Alice can use her automated testing tools and set server time using __timepatch-ts__
query parameter added to every API endpoint she want to simulate.
Here is what sample results looks like :
Alice machine -> date -u
Sat Apr 14 10:05:51 UTC 2018
Set server time.Now to 01-01-2012 12:12:00 -> curl http://127.0.0.1:8080/now?__timepatch-ts__=1325419920
Sun Jan 01 12:12:00 UTC 2012
Set server time.Now to 01-01-2024 12:12:00 -> curl http://127.0.0.1:8080/now?__timepatch-ts__=1704111120
Mon Jan 01 12:12:00 UTC 2024
normal -> curl http://127.0.0.1:8080/now
Sat Apr 14 10:05:51 UTC 2018
Conclusion
Use Solution 1 carefully as it will give freedom for user to set server time and don’t forget to read package notes. Use it for and only for testing.
Monkey patching is not only doable for dynamic language like Python, PHP or Ruby, but apparently also doable for Go although it is unsafe.
Solution 2
Do you have any suggestions/opinions?