I'm learning tests in Go and I have been trying to measure test coverage in an API that I created:
main.go
package main
import (
"encoding/json"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", SimpleGet)
log.Print("Listen port 8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// SimpleGet return Hello World
func SimpleGet(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
}
w.Header().Set("Content-Type", "application/json")
data := "Hello World"
switch r.Method {
case http.MethodGet:
json.NewEncoder(w).Encode(data)
default:
http.Error(w, "Invalid request method", 405)
}
}
And the test:
main_test.go
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestSimpleGet(t *testing.T) {
req, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
SimpleGet(w, req)
resp := w.Result()
if resp.Header.Get("Content-Type") != "application/json" {
t.Errorf("handler returned wrong header content-type: got %v want %v",
resp.Header.Get("Content-Type"),
"application/json")
}
if status := w.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
expected := `"Hello World"`
if strings.TrimSuffix(w.Body.String(), "\n") != expected {
t.Errorf("handler returned unexpected body: got %v want %v", w.Body.String(), expected)
}
}
When I run go test it is fine, the test has passed. But when I try to get the test coverage, I got this HTML:
I would like to understand what is happened here because it has not covered anything. Does anyone know to explain?
I have found my error:
I was trying to run the test coverage with these commands:
$ go test -run=Coverage -coverprofile=c.out
$ go tool cover -html=c.out
But the correct commands are:
$ go test -coverprofile=c.out
$ go tool cover -html=c.out
Result:
OBS: I wrote one more test to cover all switch statements. Thanks for all, and I'm sorry if I disturbed someone.
Related
A few days ago, I started learning Golang, PostgreSQL, and Unit tests.
I searched a lot of information about how to API unit test in GORM here and there.
I just tried to follow up Golang documentation (https://go.dev/src/net/http/httptest/example_test.go) and one of the StackOverflow posts (https://codeburst.io/unit-testing-for-rest-apis-in-go-86c70dada52d)
I am not sure I got this error message "undefined: RegisterTodoListRoutes".
handler := http.HandlerFunc(RegisterTodoListRoutes) in this line
My todolist-routes.go code is
package routes
import (
"github.com/gorilla/mux"
"github.com/jiwanjeon/go-todolist/pkg/controllers"
)
var RegisterTodoListRoutes = func (router *mux.Router){
router.HandleFunc("/todo/", controllers.CreateTodo).Methods("POST")
router.HandleFunc("/todo/", controllers.GetTodo).Methods("GET")
router.HandleFunc("/todo/{todoId}", controllers.GetTodoById).Methods("GET")
router.HandleFunc("/todo/{todoId}", controllers.UpdateTodo).Methods("PUT")
router.HandleFunc("/todo/{todoId}", controllers.DeleteTodo).Methods("DELETE")
router.HandleFunc("/complete/{todoId}", controllers.CompleteTodo).Methods("PUT")
router.HandleFunc("/incomplete/{todoId}", controllers.InCompleteTodo).Methods("PUT")
}
and my todolist-routes_test.go test code is
package routes_test
import (
"net/http/httptest"
"testing"
"net/http"
)
func TestRegisterTodoListRoutes(t *testing.T) {
req, err := http.NewRequest("GET", "/todo/", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(RegisterTodoListRoutes)
handler.ServeHTTP(rr, req)
// Check the status code is what we expect.
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
expected := `[{"ID":46,"title":"test-title-console-check","last_name":"test-description-console-check", "conditions": true]`
if rr.Body.String() != expected {
t.Errorf("handler returned unexpected body: got %v want %v",
rr.Body.String(), expected)
}
}
Here is a link for todolist_routes.go : https://go.dev/play/p/XoVMbK7PnLu
this is for test code : https://go.dev/play/p/NddwrU5m6ss
I really appreciate your help!!
Edit 1] I found the reason why I got this error. When I remove _test above at package and run it. I got this error "cannot convert Routes (type func(*mux.Router)) to type http.HandlerFunc"
I have a file in my project as :
package handlers
import (
"github.com/gorilla/mux"
)
type IHandlerProvider interface {
GetRouter() *mux.Router
}
type HandlerProvider struct{}
func (h HandlerProvider) GetRouter() *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/health", Health).Methods("GET")
return r
}
What is the right way to unit test this? For instance:
package handlers
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetRouterOk(t *testing.T) {
var subject IHandlerProvider = HandlerProvider{}
router := subject.GetRouter()
assert.NotNil(t, router)
}
I can assert the object not being null, but how can I test the routes are correct?
If you want to test that the router is returning expected handler (vs test behaviour), you can do something like the following:
r := mux.NewRouter()
r.HandleFunc("/a", handlerA).Methods("GET")
r.HandleFunc("/b", handlerB).Methods("GET")
req, err := httptest.NewRequest("GET", "http://example.com/a", nil)
require.NoError(err, "create request")
m := &mux.RouteMatch{}
require.True(r.Match(req, m), "no match")
v1 := reflect.ValueOf(m.Handler)
v2 := reflect.ValueOf(handlerA)
require.Equal(v1.Pointer(), v2.Pointer(), "wrong handler")
You could use httptest package.
handlers.go:
package handlers
import (
"net/http"
"github.com/gorilla/mux"
)
type IHandlerProvider interface {
GetRouter() *mux.Router
}
type HandlerProvider struct {}
func Health(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("ok"))
}
func (h HandlerProvider) GetRouter() *mux.Router {
r := mux.NewRouter()
r.HandleFunc("/health", Health).Methods("GET")
return r
}
handlers_test.go:
package handlers
import (
"testing"
"bytes"
"io/ioutil"
"net/http/httptest"
)
func TestGetRouterOk(t *testing.T) {
assertResponseBody := func(t *testing.T, s *httptest.Server, expectedBody string) {
resp, err := s.Client().Get(s.URL+"/health")
if err != nil {
t.Fatalf("unexpected error getting from server: %v", err)
}
if resp.StatusCode != 200 {
t.Fatalf("expected a status code of 200, got %v", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("unexpected error reading body: %v", err)
}
if !bytes.Equal(body, []byte(expectedBody)) {
t.Fatalf("response should be ok, was: %q", string(body))
}
}
var subject IHandlerProvider = HandlerProvider{}
router := subject.GetRouter()
s := httptest.NewServer(router)
defer s.Close()
assertResponseBody(t, s, "ok")
}
unit test result:
=== RUN TestGetRouterOk
--- PASS: TestGetRouterOk (0.00s)
PASS
ok github.com/mrdulin/golang/src/stackoverflow/64584472 0.097s
coverage:
Here's a minification of using the stackdriver trace go client package.
It seems like this trivial example should work but it produces an error.
package main
import (
"context"
"flag"
"log"
"net/http"
"cloud.google.com/go/trace"
"github.com/davecgh/go-spew/spew"
"google.golang.org/api/option"
)
var (
projectID = flag.String("projectID", "", "projcect id")
serviceAccountKey = flag.String("serviceAccountKey", "", "path to serviceacount json")
)
func main() {
flag.Parse()
mux := http.NewServeMux()
log.Fatalln(http.ListenAndServe(":9000", Trace(*projectID, *serviceAccountKey, mux)))
}
func Trace(projectID string, keyPath string, h http.Handler) http.HandlerFunc {
client, err := trace.NewClient(context.Background(), projectID, option.WithServiceAccountFile(keyPath))
if err != nil {
panic(err)
}
return func(w http.ResponseWriter, r *http.Request) {
s := client.SpanFromRequest(r)
defer func() { err := s.FinishWait(); spew.Dump(err) }()
h.ServeHTTP(w, r)
}
}
I'm running this as:
$ teststackdrivertrace -projectID ${GCLOUD_PROJECT_ID} -serviceAccountKey ${PATH_TO_SERVICEACCOUNT_JSON}
and curling it produces:
$ curl -s --header "X-Cloud-Trace-Context: 205445aa7843bc8bf206b120001000/0;o=1" localhost:9000
$ (*googleapi.Error)(0xc4203a2c80)(googleapi: Error 400: Request contains an invalid argument., badRequest)
What am I missing?
My traceId was 30 bytes long not 32. I took the example curl from https://cloud.google.com/trace/docs/faq which is also only 30 bytes.
I am trying to learn go with a TDD mindset. I am stuck getting my head wrapped around testing.
In the example below, I am prompting a user for input, doing a little validation and printing the results. I wrote a test for it (which is passing) however I don't feel like it is hitting the validation portion, so I am doing something wrong. Any advice would be appreciated.
https://play.golang.org/p/FDpbof9Y20
package main
import (
"bufio"
"fmt"
"io"
"os"
"regexp"
"strings"
)
func main() {
response := askQuestion("What is your name?")
fmt.Printf("Hello %s\n",response)
}
func askQuestion(question string) string {
reader := bufio.NewReader(os.Stdin)
answer := ""
for {
fmt.Printf("%s\n", question)
input, err := reader.ReadString('\n')
if err != nil {
if err != io.EOF {
panic(err)
}
break
}
if regexp.MustCompile(`[A-Z]{5}`).MatchString(strings.TrimSpace(input)) == true {
answer = strings.TrimSpace(input)
fmt.Printf("You entered %s\n", answer)
break
} else {
fmt.Printf("\033[31mYou must enter only 5 upper case letters.\n\033[0m")
continue
}
}
return answer
}
https://play.golang.org/p/WcI4CRfle5
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"reflect"
"strings"
"testing"
)
func TestAskQuestion(t *testing.T) {
expected := "foo"
entered := "foo"
askQuestion("What is your last name?")
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
fmt.Println(entered)
outC := make(chan string)
go func() {
var buf bytes.Buffer
io.Copy(&buf, r)
outC <- buf.String()
}()
w.Close()
os.Stdout = oldStdout
out := strings.TrimSpace(<-outC)
b, _ := ioutil.ReadAll(os.Stdin)
t.Log(string(b))
if !reflect.DeepEqual(expected, out) {
t.Fatalf("Test Status Failure Issue. Got: '%v' expected %s", out, expected)
}
}
Go's tests need to live in files which are named xyz_test.go, so the playground is not the right place to familiarize yourself with the unit testing feature.
If you have go installed locally, run the command go help test, to get a very brief introduction.
I've built a quick and easy API in Go that queries ElasticSearch. Now that I know it can be done, I want to do it correctly by adding tests. I've abstracted some of my code so that it can be unit-testable, but I've been having some issues mocking the elastic library, and as such I figured it would be best if I tried a simple case to mock just that.
import (
"encoding/json"
"github.com/olivere/elastic"
"net/http"
)
...
func CheckBucketExists(name string, client *elastic.Client) bool {
exists, err := client.IndexExists(name).Do()
if err != nil {
panic(err)
}
return exists
}
And now the test...
import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"testing"
)
type MockClient struct {
mock.Mock
}
func (m *MockClient) IndexExists(name string) (bool, error) {
args := m.Mock.Called()
fmt.Println("This is a thing")
return args.Bool(0), args.Error(1)
}
func TestMockBucketExists(t *testing.T) {
m := MockClient{}
m.On("IndexExists", "thisuri").Return(true)
>> r := CheckBucketExists("thisuri", m)
assert := assert.New(t)
assert.True(r, true)
}
To which I'm yielded with the following error: cannot use m (type MockClient) as type *elastic.Client in argument to CheckBucketExists.
I'm assuming this is something fundamental with my use of the elastic.client type, but I'm still too much of a noob.
This is an old question, but couldn't find the solution either.
Unfortunately, this library is implemented using a struct, that makes mocking it not trivial at all, so the options I found are:
(1) Wrap all the elastic.SearchResult Methods on an interface on your own and "proxy" the call, so you end up with something like:
type ObjectsearchESClient interface {
// ... all methods...
Do(context.Context) (*elastic.SearchResult, error)
}
// NewObjectsearchESClient returns a new implementation of ObjectsearchESClient
func NewObjectsearchESClient(cluster *config.ESCluster) (ObjectsearchESClient, error) {
esClient, err := newESClient(cluster)
if err != nil {
return nil, err
}
newClient := objectsearchESClient{
Client: esClient,
}
return &newClient, nil
}
// ... all methods...
func (oc *objectsearchESClient) Do(ctx context.Context) (*elastic.SearchResult, error) {
return oc.searchService.Do(ctx)
}
And then mock this interface and responses as you would with other modules of your app.
(2) Another option is like pointed in this blog post that is mock the response from the Rest calls using httptest.Server
for this, I mocked the handler, that consist of mocking the response from the "HTTP call"
func mockHandler () http.HandlerFunc{
return func(w http.ResponseWriter, r *http.Request) {
resp := `{
"took": 73,
"timed_out": false,
... json ...
"hits": [... ]
...json ... ,
"aggregations": { ... }
}`
w.Write([]byte(resp))
}
}
Then you create a dummy elastic.Client struct
func mockClient(url string) (*elastic.Client, error) {
client, err := elastic.NewSimpleClient(elastic.SetURL(url))
if err != nil {
return nil, err
}
return client, nil
}
In this case, I've a library that builds my elastic.SearchService and returns it, so I use the HTTP like:
...
ts := httptest.NewServer(mockHandler())
defer ts.Close()
esClient, err := mockClient(ts.URL)
ss := elastic.NewSearchService(esClient)
mockLibESClient := es_mock.NewMockSearcherClient(mockCtrl)
mockLibESClient.EXPECT().GetEmployeeSearchServices(ctx).Return(ss, nil)
where mockLibESClient is the library I mentioned, and we stub the mockLibESClient.GetEmployeeSearchServices method making it return the SearchService with that will return the expected payload.
Note: for creating the mock mockLibESClient I used https://github.com/golang/mock
I found this to be convoluted, but "Wrapping" the elastic.Client was in my point of view more work.
Question: I tried to mock it by using https://github.com/vburenin/ifacemaker to create an interface, and then mock that interface with https://github.com/golang/mock and kind of use it, but I kept getting compatibility errors when trying to return an interface instead of a struct, I'm not a Go expect at all so probably I needed to understand the typecasting a little better to be able to solve it like that. So if any of you know how to do it with that please let me know.
The elasticsearch go client Github repo contains an official example of how to mock the elasticsearch client. It basically involves calling NewClient with a configuration which stubs the HTTP transport:
client, err := elasticsearch.NewClient(elasticsearch.Config{
Transport: &mocktrans,
})
There are primarily three ways I discovered to create a Mock/Dumy ES client. My response does not include integration tests against a real Elasticsearch cluster.
You can follow this article so as to mock the response from the Rest calls using httptest.Server, to eventually create a dummy elastic.Client struct
As mentioned by the package author in this link, you can work on "specifying an interface that has two implementations: One that uses a real ES cluster, and one that uses callbacks used in testing. Here's an example to get you started:"
type Searcher interface {
Search(context.Context, SearchRequest) (*SearchResponse, error)
}
// ESSearcher will be used with a real ES cluster.
type ESSearcher struct {
client *elastic.Client
}
func (s *ESSearcher) Search(ctx context.Context, req SearchRequest) (*SearchResponse, error) {
// Use s.client to run against real ES cluster and perform a search
}
// MockedSearcher can be used in testing.
type MockedSearcher struct {
OnSearch func(context.Context, SearchRequest) (*SearchResponse, error)
}
func (s *ESSearcher) Search(ctx context.Context, req SearchRequest) (*SearchResponse, error) {
return s.OnSearch(ctx, req)
}
Finally, as mentioned by the author in the same link you can "run a real Elasticsearch cluster while testing. One particular nice way might be to start the ES cluster during testing with something like github.com/ory/dockertest. Here's an example to get you started:"
package search
import (
"context"
"fmt"
"log"
"os"
"testing"
"github.com/olivere/elastic/v7"
"github.com/ory/dockertest/v3"
"github.com/ory/dockertest/v3/docker"
)
// client will be initialize in TestMain
var client *elastic.Client
func TestMain(m *testing.M) {
pool, err := dockertest.NewPool("")
if err != nil {
log.Fatalf("unable to create new pool: %v", err)
}
options := &dockertest.RunOptions{
Repository: "docker.elastic.co/elasticsearch/elasticsearch-oss",
Tag: "7.8.0",
PortBindings: map[docker.Port][]docker.PortBinding{
"9200": {{HostPort: "9200"}},
},
Env: []string{
"cluster.name=elasticsearch",
"bootstrap.memory_lock=true",
"discovery.type=single-node",
"network.publish_host=127.0.0.1",
"logger.org.elasticsearch=warn",
"ES_JAVA_OPTS=-Xms1g -Xmx1g",
},
}
resource, err := pool.RunWithOptions(options)
if err != nil {
log.Fatalf("unable to ES: %v", err)
}
endpoint := fmt.Sprintf("http://127.0.0.1:%s", resource.GetPort("9200/tcp"))
if err := pool.Retry(func() error {
var err error
client, err = elastic.NewClient(
elastic.SetURL(endpoint),
elastic.SetSniff(false),
elastic.SetHealthcheck(false),
)
if err != nil {
return err
}
_, _, err = client.Ping(endpoint).Do(context.Background())
if err != nil {
return err
}
return nil
}); err != nil {
log.Fatalf("unable to connect to ES: %v", err)
}
code := m.Run()
if err := pool.Purge(resource); err != nil {
log.Fatalf("unable to stop ES: %v", err)
}
os.Exit(code)
}
func TestAgainstRealCluster(t *testing.T) {
// You can use "client" variable here
// Example code:
exists, err := client.IndexExists("cities-test").Do(context.Background())
if err != nil {
t.Fatal(err)
}
if !exists {
t.Fatal("expected to find ES index")
}
}
The line
func CheckBucketExists(name string, client *elastic.Client) bool {
states that CheckBucketExists expects a *elastic.Client.
The lines:
m := MockClient{}
m.On("IndexExists", "thisuri").Return(true)
r := CheckBucketExists("thisuri", m)
pass a MockClient to the CheckBucketExists function.
This is causing a type conflict.
Perhaps you need to import github.com/olivere/elastic into your test file and do:
m := &elastic.Client{}
instead of
m := MockClient{}
But I'm not 100% sure what you're trying to do.