- 2018/04/14

Imagine we build REST API server using Go with following specs :

  • Feature Bucket Create with endpoint POST /buckets and want to set our Bucket entity created_at datetime value from our code as the request arrive using time.Now().
  • Feature Bucket List with endpoint GET /buckets and will return all bucket which created_at datetime value is between time.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?