Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Golden File Feature #195

Merged
merged 5 commits into from
Nov 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions diagram_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ func TestWebSequenceDiagramGeneratesDSL(t *testing.T) {

actual := wsd.String()

expected := `"cli"->"sut": (1) request1
"sut"->"C": (2) request2
"C"->>"sut": (3) response1
"sut"->>"cli": (4) response2
expected := `"client"->"server": (1) request1
"server"->"C": (2) request2
"C"->>"server": (3) response1
"server"->>"client": (4) response2
`
if expected != actual {
t.Fatalf("expected=%s != \nactual=%s", expected, actual)
Expand Down Expand Up @@ -140,9 +140,7 @@ func TestWebSequenceDiagramGeneratesDSL(t *testing.T) {
}

func TestNewSequenceDiagramFormatterStoragePath(t *testing.T) {
t.Parallel()
t.Run("should use default storage path", func(t *testing.T) {
t.Parallel()
formatter := SequenceDiagram()
v, ok := formatter.(*SequenceDiagramFormatter)
if !ok {
Expand All @@ -152,7 +150,6 @@ func TestNewSequenceDiagramFormatterStoragePath(t *testing.T) {
})

t.Run("should use custom storage path", func(t *testing.T) {
t.Parallel()
formatter := SequenceDiagram(".sequence-diagram")
v, ok := formatter.(*SequenceDiagramFormatter)
if !ok {
Expand Down Expand Up @@ -502,8 +499,6 @@ func Test_toImageExt(t *testing.T) {
}

func Test_imageName(t *testing.T) {
t.Parallel()

type args struct {
name string
contentType string
Expand All @@ -527,7 +522,6 @@ func Test_imageName(t *testing.T) {
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := imageName(tt.args.name, tt.args.contentType, tt.args.index); got != tt.want {
t.Errorf("imageName() = %v, want %v", got, tt.want)
}
Expand Down
6 changes: 3 additions & 3 deletions doc/markdow_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
```mermaid
sequenceDiagram
autonumber
cli->>sut: GET /image
sut-->>cli: 200
client->>server: GET /image
server-->>client: 200
```

## Event log
#### Event 1

GET /image HTTP/1.1
Host: sut
Host: server



Expand Down
36 changes: 31 additions & 5 deletions doc/spectest.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
## Use Cases of spectest
# Use Cases of spectest
## Installation of spectest cli
```shell
go install github.com/go-spectest/spectest/cmd/spectest@latest
```

## Generating Markdown documents from E2E test results
The spectest offers numerous features that are not available in its forked counterpart, [steinfletcher/apitest](https://github.com/steinfletcher/apitest). While apitest was a simple library, spectest provides functionalities both as a library and a CLI (Command Line Interface).

The current version of spectest is gradually upgrading its documentation generation capabilities. Instead of generating HTML documents, it aims to preserve End-to-End (E2E) test results as documents in Markdown format for developers working on GitHub.
Expand Down Expand Up @@ -44,8 +50,28 @@ spectest index docs --title "naraku api result"
[Output](https://github.com/go-spectest/naraku/blob/main/docs/index.md):
![index_result](./image/index.png)

## Installation of spectest cli
```shell
go install github.com/go-spectest/spectest/cmd/spectest@latest
```

## Use golden file for E2E test
Golden File reduces your effort to create expected value data. The spectest can use a Golden File as the response body for the expected value. The Golden File will be overwritten with the actual response data in one of the following cases;
- If the Golden File does not exist in the specified path
- If you run the test with `go test -update . /...`

### How to use
```go
handler := http.NewServeMux()
handler.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
if _, err := w.Write([]byte(`{"a": 12345}`)); err != nil {
t.Fatal(err)
}
})

spectest.New().
Handler(handler).
Get("/hello").
Expect(t).
BodyFronGoldenFile(filepath.Join("testdata", "golden.json")). // use golden file
Status(http.StatusOK).
End()
```
48 changes: 48 additions & 0 deletions file_system.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package spectest

import (
"errors"
"os"
"path/filepath"
)
Expand All @@ -25,3 +26,50 @@ func (r *defaultFileSystem) create(name string) (*os.File, error) {
func (r *defaultFileSystem) mkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}

// goldenFile is a file that is used to store the golden data.
type goldenFile struct {
// path is the path to the golden file
path string
// update is a flag that indicates if the golden file should be updated
update bool
// fs is the file system used to create the golden file
fs fileSystem
}

// newGoldenFile creates a new golden file
func newGoldenFile(path string, update bool, fs fileSystem) *goldenFile {
return &goldenFile{
path: path,
update: update,
fs: fs,
}
}

// read reads the golden file.
func (g *goldenFile) read() ([]byte, error) {
return os.ReadFile(filepath.Clean(g.path))
}

// write writes the given data to the golden file.
func (g *goldenFile) write(data []byte) error {
if !g.update {
return errors.New("golden file update is disabled")
}

dir := filepath.Dir(g.path)
if err := g.fs.mkdirAll(filepath.Clean(dir), os.ModePerm); err != nil {
return err
}

f, err := g.fs.create(g.path)
if err != nil {
return err
}
defer f.Close() // nolint

if _, err := f.Write(data); err != nil {
return err
}
return nil
}
47 changes: 31 additions & 16 deletions file_system_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,62 @@ package spectest

import (
"os"
"path/filepath"
"testing"
)

func Test_defaultFileSystem_create(t *testing.T) {
t.Parallel()

t.Run("should create a file", func(t *testing.T) {
t.Parallel()
if err := os.Chdir(os.TempDir()); err != nil {
t.Fatal(err)
}
tempDir := os.TempDir()

fs := &defaultFileSystem{}
file, err := fs.create("test.txt")
file, err := fs.create(filepath.Join(tempDir, "test.txt"))
if err != nil {
t.Fatalf("create() error = %v, wantErr %v", err, false)
}
defer file.Close() // nolint

if _, err := os.Stat("test.txt"); os.IsNotExist(err) {
if _, err := os.Stat(filepath.Join(tempDir, "test.txt")); os.IsNotExist(err) {
t.Errorf("create() file does not exist")
}
})
}

func Test_defaultFileSystem_mkdirAll(t *testing.T) {
t.Parallel()

t.Run("should create a directory", func(t *testing.T) {
t.Parallel()
if err := os.Chdir(os.TempDir()); err != nil {
t.Fatal(err)
}
tempDir := os.TempDir()

fs := &defaultFileSystem{}
if err := fs.mkdirAll("test", 0755); err != nil {
if err := fs.mkdirAll(filepath.Join(tempDir, "test"), 0755); err != nil {
t.Fatalf("mkdirAll() error = %v, wantErr %v", err, false)
}

if _, err := os.Stat("test"); os.IsNotExist(err) {
if _, err := os.Stat(filepath.Join(tempDir, "test")); os.IsNotExist(err) {
t.Errorf("mkdirAll() directory does not exist")
}
})
}

func Test_goldenFile_write(t *testing.T) {
t.Run("should write data to the golden file", func(t *testing.T) {
tempDir := os.TempDir()

g := newGoldenFile(filepath.Join(tempDir, "test.txt"), true, &defaultFileSystem{})
if err := g.write([]byte("test")); err != nil {
t.Fatalf("write() error = %v, wantErr %v", err, false)
}

if _, err := os.Stat(filepath.Join(tempDir, "test.txt")); os.IsNotExist(err) {
t.Errorf("write() file does not exist")
}
})

t.Run("should not write data to the golden file", func(t *testing.T) {
tempDir := os.TempDir()

g := newGoldenFile(filepath.Join(tempDir, "test.txt"), false, &defaultFileSystem{})
if err := g.write([]byte("test")); err == nil {
t.Fatalf("write() error = %v, wantErr %v", err, true)
}
})
}
4 changes: 2 additions & 2 deletions meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import (
)

// ConsumerDefaultName default consumer name
const ConsumerDefaultName = "cli"
const ConsumerDefaultName = "client"

// SystemUnderTestDefaultName default name for system under test
const SystemUnderTestDefaultName = "sut"
const SystemUnderTestDefaultName = "server"

// Meta represents the meta data for the report.
type Meta struct {
Expand Down
33 changes: 28 additions & 5 deletions mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type Transport struct {
// httpClient is the http client used when networking is enabled.
httpClient *http.Client
// mocks is the list of mocks to use when mocking the request
mocks []*Mock
mocks Mocks
// mockResponseDelayEnabled will enable mock response delay
mockResponseDelayEnabled bool
// observers is the list of observers to use when observing the request and response
Expand All @@ -43,7 +43,7 @@ type Transport struct {
// If you set httpClient to nil, http.DefaultClient will be used.
// If you set debug to nil, debug will be disabled.
func newTransport(
mocks []*Mock,
mocks Mocks,
httpClient *http.Client,
debug *debug,
mockResponseDelayEnabled bool,
Expand Down Expand Up @@ -176,6 +176,29 @@ type Mock struct {
execCount *execCount
}

// Mocks is a slice of Mock
type Mocks []*Mock

// len returns the length of the mocks
func (mocks Mocks) len() int {
return len(mocks)
}

// findUnmatchedMocks returns a list of unmatched mocks.
// An unmatched mock is a mock that was not used, e.g. there was not a matching http Request for the mock
func (mocks Mocks) findUnmatchedMocks() []UnmatchedMock {
var unmatchedMocks []UnmatchedMock
for _, m := range mocks {
if !m.state.isRunning() {
unmatchedMocks = append(unmatchedMocks, UnmatchedMock{
URL: *m.request.url,
})
break
}
}
return unmatchedMocks
}

// Matches checks whether the given request matches the mock
func (m *Mock) Matches(req *http.Request) []error {
var errs []error
Expand Down Expand Up @@ -275,7 +298,7 @@ func newMockResponse(m *Mock) *MockResponse {

// StandaloneMocks for using mocks outside of API tests context
type StandaloneMocks struct {
mocks []*Mock
mocks Mocks
httpClient *http.Client
debug *debug
}
Expand Down Expand Up @@ -465,7 +488,7 @@ func (m *Mock) parseURL(u string) {
}

// matches checks whether the given request matches any of the given mocks
func matches(req *http.Request, mocks []*Mock) (*MockResponse, error) {
func matches(req *http.Request, mocks Mocks) (*MockResponse, error) {
mockError := newUnmatchedMockError()
for mockNumber, mock := range mocks {
mock.m.Lock() // lock is for isUsed when matches is called concurrently by RoundTripper
Expand Down Expand Up @@ -741,7 +764,7 @@ func (r *MockResponse) End() *Mock {
// EndStandalone finalizes the response definition of standalone mocks
func (r *MockResponse) EndStandalone(other ...*Mock) func() {
transport := newTransport(
append([]*Mock{r.mock}, other...),
append(Mocks{r.mock}, other...),
r.mock.httpClient,
r.mock.debugStandalone,
false,
Expand Down
13 changes: 7 additions & 6 deletions mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
"path/filepath"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -733,7 +734,7 @@ func TestMocksAddMatcher(t *testing.T) {
Status(http.StatusOK).
End()

mockResponse, matchErrors := matches(req, []*Mock{testMock})
mockResponse, matchErrors := matches(req, Mocks{testMock})

assert.Equal(t, test.matchErrors, matchErrors)
if test.mockResponse == nil {
Expand Down Expand Up @@ -788,10 +789,10 @@ func TestMocksMatches(t *testing.T) {
Get("/user/1234").
RespondWith().
Status(http.StatusOK).
BodyFromFile("testdata/mock_response_body.json").
BodyFromFile(filepath.Join("testdata", "mock_response_body.json")).
End()

mockResponse, matchErrors := matches(req, []*Mock{getUser, getPreferences})
mockResponse, matchErrors := matches(req, Mocks{getUser, getPreferences})

assert.Equal(t, true, matchErrors == nil)
assert.Equal(t, true, mockResponse != nil)
Expand All @@ -815,7 +816,7 @@ func TestMocksMatchesErrors(t *testing.T) {
Status(http.StatusOK).
End()

mockResponse, matchErrors := matches(req, []*Mock{testMock})
mockResponse, matchErrors := matches(req, Mocks{testMock})

assert.Equal(t, true, mockResponse == nil)
assert.Equal(t, &unmatchedMockError{errors: map[int][]error{
Expand All @@ -832,7 +833,7 @@ func TestMocksMatchesErrors(t *testing.T) {
func TestMocksMatchesNilIfNoMatch(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/preferences/12345", nil)

mockResponse, matchErrors := matches(req, []*Mock{})
mockResponse, matchErrors := matches(req, Mocks{})

if mockResponse != nil {
t.Fatal("Expected nil")
Expand All @@ -857,7 +858,7 @@ func TestMocksMatchesErrorsMatchUnmatchedMocks(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/preferences/12345", nil)

mockResponse, matchErrors := matches(req,
[]*Mock{
Mocks{
NewMock().
Get("/preferences/123456").
RespondWith().
Expand Down
Loading