Skip to content

Commit

Permalink
Merge pull request #195 from go-spectest/feat/goleden
Browse files Browse the repository at this point in the history
Golden File Feature
  • Loading branch information
nao1215 authored Nov 8, 2023
2 parents a94f065 + 19d6685 commit 6fa3c12
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 90 deletions.
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

0 comments on commit 6fa3c12

Please sign in to comment.