I have AWS lambdas in my golang app.
Wanted to write unit tests for it:
func TestHandleLambdaEvent(t *testing.T) {
ctx := context.TODO()
//mockNewAuthContextWithMap()
oldNewAuthContextWithMap := NewAuthContextWithMap
defer func() { NewAuthContextWithMap = oldNewAuthContextWithMap }()
NewAuthContextWithMap = func(stringifiedMap map[string]interface{}) (*authutils.AuthContext, error) {
return &authutils.AuthContext{UserID: "12345", Org: "XYZOrg", Role: "Member", Timestamp: 999999999}, nil
}
//mockLambdaInvoke()
old := LambdaInvoke
defer func() { LambdaInvoke = old }()
LambdaInvoke = func(context context.Context, arn string, request, response interface{}) error { return nil }
resp, err := handleLambdaEvent(ctx, events.APIGatewayProxyRequest{})
if err != nil {
t.Fatalf("handleLambdaEvent returned error: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("Invalid status code, provded: %d required %d", resp.StatusCode, http.StatusOK)
}
}
this test code works fine, but.. I would like to put code under mockNewAuthContextWithMap to func ie:
func mockNewAuthContextWithMap() {
old := NewAuthContextWithMap
defer func() { NewAuthContextWithMap = old }()
NewAuthContextWithMap = func(stringifiedMap map[string]interface{}) (*authutils.AuthContext, error) {
return &authutils.AuthContext{UserID: "12345", Org: "XYZOrg", Role: "Member", Timestamp: 999999999}, nil
}
}
and simply call it from the test. Then it does not work.
I assume its due to defer func, which simply run before test will call handler.
how can I fix it ?
looks like I can reuse this part of code:
NewAuthContextWithMap = func(stringifiedMap map[string]interface{}) (*authutils.AuthContext, error) {
return &authutils.AuthContext{UserID: "12345", Org: "XYZOrg", Role: "Member", Timestamp: 999999999}, nil
}
but saving old function value with defer func that will rollback it must be repeated everytime.
EDIT:
probably only what I can do is:
func TestHandleLambdaEvent(t *testing.T) {
ctx := context.TODO()
mockNewAuthContextWithMap()
mockLambdaInvoke()
resp, err := handleLambdaEvent(ctx, events.APIGatewayProxyRequest{})
if err != nil {
t.Fatalf("handleLambdaEvent returned error: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("Invalid status code, provded: %d required %d", resp.StatusCode, http.StatusOK)
}
defer RollbackExternalMethods()
}
// LambdaInvoke
func mockNewAuthContextWithMap() {
NewAuthContextWithMap = func(stringifiedMap map[string]interface{}) (*authutils.AuthContext, error) {
return &authutils.AuthContext{UserID: "12345", Org: "XYZOrg", Role: "Member", Timestamp: 999999999}, nil
}
}
// LambdaInvoke
func mockLambdaInvoke() {
LambdaInvoke = func(context context.Context, arn string, request, response interface{}) error { return nil }
}
func RollbackExternalMethods() {
NewAuthContextWithMap = authutils.NewAuthContextWithMap
LambdaInvoke = lambdaClient.Invoke
}
EDIT: handleLambdaEvent
func handleLambdaEvent(context context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
authContext, err := NewAuthContextWithMap(request.RequestContext.Authorizer)
if err != nil {
fmt.Println("Error parsing auth context:", err)
return awsutils.StatusResponse(http.StatusInternalServerError), nil
}
queryRequest := handlerInput.GetProfile{
Type: handlerInput.TypeGetProfile,
UserId: authContext.UserID,
}
queryResp := dbModel.User{}
err = LambdaInvoke(context, userServiceArn, queryRequest, &queryResp)
if err != nil {
if ierrError, ok := err.(ierr.Error); ok {
if ierrError.IsSame(user.RecordNotFoundError) {
fmt.Printf("Could not find user profile of userId: %s \n", authContext.UserID)
emptyResp := dbModel.User{}
return awsutils.SwaggerResponse(http.StatusOK, emptyResp.SwaggerModel()), nil
}
}
fmt.Println("Error invoking lambda:", err)
return awsutils.StatusResponse(http.StatusInternalServerError), nil
}
swagUser := queryResp.SwaggerModel()
return awsutils.SwaggerResponse(http.StatusOK, swagUser), nil
}
https://github.com/stretchr/testify#suite-package
testify has nice features like before, after test etc. by using this its easy to avoid copy pasting the sam parts of code
Related
I have a function designed to listen to a Nats subject and route the messages as it receives them:
func (conn *JetStreamConnection) SubscribeMultiple(ctx context.Context, subject string,
subscribers ...*SubscriptionCallback) error {
callbacks := make(map[string]func(*pnats.NatsMessage) (func(context.Context), error))
for _, subscriber := range subscribers {
callbacks[subscriber.Category] = subscriber.Callback
}
fullSubject := fmt.Sprintf("%s.*", subject)
sub, err := conn.context.SubscribeSync(fullSubject, nats.Context(ctx))
if err != nil {
return err
}
loop:
for {
select {
case <-ctx.Done():
break loop
default:
}
msg, err := sub.NextMsgWithContext(ctx)
if err != nil {
return err
}
msg.InProgress()
var message pnats.NatsMessage
if err := conn.unmarshaller(msg.Data, &message); err != nil {
msg.Term()
return err
}
actualSubject := fmt.Sprintf("%s.%s", subject, message.Context.Category)
subscriber, ok := callbacks[message.Context.Category]
if !ok {
msg.Nak()
continue
}
callback, err := subscriber(&message)
if err == nil {
msg.Ack()
} else {
msg.Nak()
return err
}
callback(ctx)
}
if err := sub.Unsubscribe(); err != nil {
return err
}
return nil
}
My problem is that, since the SubscribeSync function produces a *nats.Subscription object, I have no way to mock out the test. How can I test around this object?
You can put your loop in a separate function. This func can accept an interface that describes nats Subscription instead of *nats.Subscription. This way you will be able to create Subscription mocks with gomock or other tools. After that you can test the inside func separately
Something like this:
func (conn *JetStreamConnection) SubscribeMultiple(ctx context.Context, subject string,
subscribers ...*SubscriptionCallback) error {
callbacks := make(map[string]func(*pnats.NatsMessage) (func(context.Context), error))
for _, subscriber := range subscribers {
callbacks[subscriber.Category] = subscriber.Callback
}
fullSubject := fmt.Sprintf("%s.*", subject)
sub, err := conn.context.SubscribeSync(fullSubject, nats.Context(ctx))
if err != nil {
return err
}
return run(ctx, sub)
}
//go:generate mockgen -source conn.go -destination ../mocks/conn.go -package mocks
type ISubscription interface{
NextMsgWithContext(ctx context.Context) (*nats.Msg, error)
Unsubscribe() error
}
func (conn *JetStreamConnection) run(ctx context.Context, sub ISubscription) error {
loop:
for {
select {
case <-ctx.Done():
break loop
default:
}
msg, err := sub.NextMsgWithContext(ctx)
if err != nil {
return err
}
msg.InProgress()
var message pnats.NatsMessage
if err := conn.unmarshaller(msg.Data, &message); err != nil {
msg.Term()
return err
}
actualSubject := fmt.Sprintf("%s.%s", subject, message.Context.Category)
subscriber, ok := callbacks[message.Context.Category]
if !ok {
msg.Nak()
continue
}
callback, err := subscriber(&message)
if err == nil {
msg.Ack()
} else {
msg.Nak()
return err
}
callback(ctx)
}
if err := sub.Unsubscribe(); err != nil {
return err
}
}
upd: if you still want to test SubscribeMultiple, you can create a Runner that will have only one func Run and take it as dependency for JetStreamConnection. Again, you can create a mock for Runner and test with it
I have created a CSV export function for my to-do list application. The function is working, the handler is returning a written file but I get a strange panic in the console from the Gin framework:
http: wrote more than the declared Content-Length
Is that something crucial and how can I fix this panic.
This is my function:
func (r *Repository) CSVExport() (*os.File, error) {
tasks, err := r.getAllTasks()
if err != nil {
return nil, err
}
file, err := os.Create("tasks.csv")
if err != nil {
return nil, err
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
var taskNameList []string
for _, task := range tasks {
taskNameList = append(taskNameList, task.Text)
}
err = writer.Write(taskNameList)
if err != nil {
return nil, err
}
return file, nil
}
And this is the handler:
func CSVExport(data model.ListOperations) gin.HandlerFunc {
return func(c *gin.Context) {
tasks, err := data.CSVExport()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Task"})
}
c.FileAttachment("./tasks.csv", "tasks.csv")
c.Writer.Header().Set("attachment", "filename=tasks.csv")
c.JSON(200, tasks)
}
}
Your code has some error:
You need to return on error
You can't return JSON after returning your file with fileAttachment (it already does this stuff)
func CSVExport(data model.ListOperations) gin.HandlerFunc {
return func(c *gin.Context) {
tasks, err := data.CSVExport()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Task"})
return //stop it on error
}
c.FileAttachment("./tasks.csv", "tasks.csv")
c.Writer.Header().Set("attachment", "filename=tasks.csv")
//c.JSON(200, tasks) not need the fileAttachement func do it
}
}
Say I have several different gRPC servers, for example x.Server, y.Server and z.Server, and in order to spin them up, I have a lot of repeated code inside their main function, e.g.:
func main() {
if err := config.EnsureArgLength(1); err != nil {
log.Fatalln(err.Error())
}
srv := &x.Server{}
if err := srv.ReadServerConfig(os.Args[1]); err != nil {
log.Fatalln(err.Error())
}
if err := srv.RegisterListener(); err != nil {
log.Fatalln(err.Error())
}
if err := srv.RegisterClients(); err != nil {
log.Fatalln(err.Error())
}
s := grpc.NewServer()
proto.RegisterXServer(s, srv)
if err := srv.Serve(s); err != nil {
log.Fatalf("failed to serve: %s", err.Error())
}
}
I would love to refactor this main function to make it one or two lines long, something like the following:
func main() {
srv := x.Server{}
if err := srv.RegisterAndServe(); err != nil {
log.Fatal("failed to serve: %s", err.Error())
}
}
But each server will have an auto-generated function proto.RegisterXServer which is not part of x.Server struct, and I'm also not able to modify the file which contains it, since it is auto generated. How should I proceed?
in regards to op changes, which was radical,
I can suggest using a reducer pattern like this.
package main
import (
"fmt"
)
func main() {
fail(reduce(sayHello(), sayGoodbye))
}
func sayHello() func() error {
return func() error { fmt.Println("Hello, playground"); return nil }
}
func sayGoodbye() error {
fmt.Println("Goodbye from the playground")
return nil
}
func reduce(h ...func() error) error {
for _, hh := range h {
if err := hh(); err != nil {
return err
}
}
return nil
}
func fail(err error) {
if err != nil {
panic(err)
}
}
I am confusing the following code, I didn't see the 1st go routine has any returns in TimelineItemStream function, and 2nd go routine call the unsub. In Subscriber interface define the unsub callbackup function.
the code is come from git clone https://github.com/nicolasparada/nakama.git
// TimelineItem model
type TimelineItem struct {
ID string `json:"id"`
UserID string `json:"-"`
PostID string `json:"-"`
Post *Post `json:"post,omitempty"`
}
// Subscriber interface.
type Subscriber interface {
Sub(topic string, cb func(data []byte)) (unsub func() error, err error)
}
func (s *Service) TimelineItemStream(ctx context.Context) (<-chan TimelineItem, error) {
uid, ok := ctx.Value(KeyAuthUserID).(string)
if !ok {
return nil, ErrUnauthenticated
}
tt := make(chan TimelineItem)
unsub, err := s.pubsub.Sub(timelineTopic(uid), func(data []byte) {
go func(r io.Reader) {
var ti TimelineItem
err := gob.NewDecoder(r).Decode(&ti)
if err != nil {
log.Printf("could not gob decode timeline item: %v\n", err)
return
}
tt <- ti
}(bytes.NewReader(data))
})
if err != nil {
return nil, fmt.Errorf("could not subscribe to timeline: %w", err)
}
go func() {
<-ctx.Done()
if err := unsub(); err != nil {
log.Printf("could not unsubcribe from timeline: %v\n", err)
// don't return
}
close(tt)
}()
return tt, nil
}
In the block of call
unsub, err := s.pubsub.Sub(timelineTopic(uid), func(data []byte) {
go func(r io.Reader) {
var ti TimelineItem
err := gob.NewDecoder(r).Decode(&ti)
if err != nil {
log.Printf("could not gob decode timeline item: %v\n", err)
return
}
tt <- ti
}(bytes.NewReader(data))
})
s.pubsub.Sub returns a callback function, but in this block go routine has not any return , how to explain the block of code
Here I am trying to write a test for a REST client, by writing the apiOutput into the http.ResponseWriter but I always receive {nil nil} as apiResponse.
Can someone help me in pointing out the error ?
func Test_Do(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
t.Cleanup(server.Close)
config := NewClientConfig()
config.BaseURL = server.URL
client, err := NewClient(config)
if err != nil {
t.Fatal(err)
}
apiResponse := struct {
Id string `json:"id"`
Error error `json:"error"`
Data interface{} `json:"data"`
}{}
apiOutput := []byte(`{
"id":"1",
"error":nil,
"data":[{"IP": "1.2.2.3"}]}`)
mux.HandleFunc("/api/v1/hosts", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(apiOutput)
})
t.Run("Get Host Details", func(t *testing.T) {
req, err := client.MakeGetRequest(http.MethodGet, "/api/v1/hosts", nil)
if err != nil {
t.Fatal(err)
}
resp, err1 := client.Do(req, &apiResponse)
if err1 != nil {
t.Fatalf("failed with statusCode: %d, error: %s", resp.StatusCode, err1)
}
if apiResponse.Id != "1" || apiResponse.Error != nil {
t.Errorf("Client.Do() problem unmarshaling problem: got %#v", apiResponse)
}
fmt.Printf("%v", apiResponse)
})
}
func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if errorResponse := c.CheckResponse(resp); errorResponse != nil {
return resp, errorResponse
}
if v != nil {
if w, ok := v.(io.Writer); ok {
io.Copy(w, resp.Body)
} else {
decErr := json.NewDecoder(resp.Body).Decode(v)
if decErr == io.EOF {
decErr = nil // ignore EOF errors caused by empty response body
}
if decErr != nil {
err = decErr
}
}
}
return resp, err
}
Output:
Test_Do/Get_Host_Details: clients_test.go:269: Client.Do() problem unmarshaling problem: got struct { Id string "json:\"id\""; Error error "json:\"error\""; Data interface {} "json:\"data\"" }{Id:"", Error:error(nil), Data:interface {}(nil)}
{ <nil> <nil>}