CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/go-api-testing

Go API testing patterns -- httptest setup, table-driven tests with subtests, test helpers, middleware testing, dependency injection with interfaces, database isolation, parallel tests, testify assertions, golden files

98

1.06x
Quality

98%

Does it follow best practices?

Impact

99%

1.06x

Average score across 5 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

task.mdevals/scenario-4/

Write Table-Driven Validation Tests for a Go Product API

Problem/Feature Description

An e-commerce platform has a Go API for managing products. Products have a name, price (must be positive), category (must be one of: electronics, clothing, food, books), and optional description. The create endpoint validates all these fields and returns specific error messages.

The team wants comprehensive validation tests that cover every invalid input combination. They have asked you to write table-driven tests -- the standard Go idiom for testing multiple inputs -- with t.Run subtests so each case can be run and debugged individually.

Output Specification

Produce the following files:

  • handlers_test.go -- table-driven validation tests and happy path tests
  • testhelpers_test.go -- reusable test helpers

Focus on the validation tests for POST /api/products. Use table-driven tests with at least 5 different invalid input variations plus at least 1 valid case.

Input Files

The following files are provided as inputs. Extract them before beginning.

=============== FILE: go.mod =============== module productapi

go 1.23

=============== FILE: main.go =============== package main

import ( "encoding/json" "log" "net/http" "strconv" "sync" )

type Product struct { ID int json:"id" Name string json:"name" Price float64 json:"price" Category string json:"category" Description string json:"description,omitempty" }

var ( products = make(map[int]*Product) nextID = 1 productsMu sync.Mutex )

func main() { mux := setupRoutes() log.Println("Server starting on :8080") log.Fatal(http.ListenAndServe(":8080", mux)) }

func setupRoutes() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("GET /api/products", handleListProducts) mux.HandleFunc("POST /api/products", handleCreateProduct) mux.HandleFunc("GET /api/products/{id}", handleGetProduct) mux.HandleFunc("DELETE /api/products/{id}", handleDeleteProduct) return mux }

func resetState() { productsMu.Lock() defer productsMu.Unlock() products = make(map[int]*Product) nextID = 1 }

var validCategories = map[string]bool{ "electronics": true, "clothing": true, "food": true, "books": true, }

func handleListProducts(w http.ResponseWriter, r *http.Request) { productsMu.Lock() defer productsMu.Unlock()

list := make([]*Product, 0, len(products))
for _, p := range products {
	list = append(list, p)
}
writeJSON(w, http.StatusOK, map[string]any{"data": list})

}

func handleCreateProduct(w http.ResponseWriter, r *http.Request) { var body struct { Name string json:"name" Price float64 json:"price" Category string json:"category" Description string json:"description" } if err := json.NewDecoder(r.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "invalid JSON body") return } if body.Name == "" { writeError(w, http.StatusBadRequest, "name is required") return } if body.Price <= 0 { writeError(w, http.StatusBadRequest, "price must be greater than zero") return } if body.Category == "" { writeError(w, http.StatusBadRequest, "category is required") return } if !validCategories[body.Category] { writeError(w, http.StatusBadRequest, "category must be one of: electronics, clothing, food, books") return }

productsMu.Lock()
defer productsMu.Unlock()

p := &Product{
	ID:          nextID,
	Name:        body.Name,
	Price:       body.Price,
	Category:    body.Category,
	Description: body.Description,
}
products[nextID] = p
nextID++

writeJSON(w, http.StatusCreated, map[string]any{"data": p})

}

func handleGetProduct(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { writeError(w, http.StatusBadRequest, "invalid product ID") return }

productsMu.Lock()
defer productsMu.Unlock()

p, ok := products[id]
if !ok {
	writeError(w, http.StatusNotFound, "product not found")
	return
}
writeJSON(w, http.StatusOK, map[string]any{"data": p})

}

func handleDeleteProduct(w http.ResponseWriter, r *http.Request) { idStr := r.PathValue("id") id, err := strconv.Atoi(idStr) if err != nil { writeError(w, http.StatusBadRequest, "invalid product ID") return }

productsMu.Lock()
defer productsMu.Unlock()

if _, ok := products[id]; !ok {
	writeError(w, http.StatusNotFound, "product not found")
	return
}
delete(products, id)
w.WriteHeader(http.StatusNoContent)

}

func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) }

func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]any{"error": map[string]any{"message": message}}) }

evals

tile.json