Skip to content

Commit

Permalink
Update HACKING.md
Browse files Browse the repository at this point in the history
  • Loading branch information
gavv committed Feb 4, 2023
1 parent c1e0a61 commit d159043
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 3 deletions.
95 changes: 92 additions & 3 deletions HACKING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# Hacking guidelines

## Developer dependencies
<!-- toc -->

- [Instructions](#instructions)
* [Developer dependencies](#developer-dependencies)
* [Makefile targets](#makefile-targets)
- [Internals](#internals)
* [Object tree](#object-tree)
* [Failure reporting](#failure-reporting)
- [Code style](#code-style)
* [Comment formatting](#comment-formatting)

<!-- tocstop -->

## Instructions

### Developer dependencies

To develop httpexpect, you need:

Expand All @@ -10,7 +25,7 @@ To develop httpexpect, you need:

`go install golang.org/x/tools/cmd/stringer@latest`

## Makefile targets
### Makefile targets

Re-generate, build, lint, and test everything:

Expand Down Expand Up @@ -48,7 +63,81 @@ Run go mod tidy:
make tidy
```

## Comment formatting
Generate TOC in HACKING.md:

```
make toc
```

## Internals

### Object tree

The typical user workflow looks like this:

* create `Expect` instance (root object) using `httpexpect.Default` or `httpexpect.WithConfig`
* use `Expect` methods to create `Request` instance (HTTP request builder)
* use `Request` methods to configure HTTP request (e.g. `WithHeader`, `WithText`)
* use `Request.Expect()` method to send HTTP request and receive HTTP response; the method returns `Response` instance (HTTP response matcher)
* use `Response` methods to make assertions on HTTP response
* use `Response` methods to create child matcher objects for HTTP response payload (e.g. `Response.Headers()` or `Response.Body()`)
* use methods of matcher objects to make assertions on payload, or to create nested child matcher objects

All objects described above are linked into a tree using `chain` struct:

* every object has `chain` field
* when a child object is created (e.g. `Expect` creates `Request`, `Request` creates `Response`, and so on), the child object clones `chain` of its parent and stores it inside its `chain` field
* when an object performs an assertion (e.g. user calls `IsEqual`), it creates a temporary clone of its `chain` and uses it to report failure or success

`chain` maintains context needed to report succeeded or failed assertions:

* `AssertionContext` defines *where* the assertion happens: path to the assertion in object tree, pointer to current request and response, etc.
* `AssertionHandler` defines *what* to do in response to success or failure
* `AssertionSeverity` defines *how* to treat failures, either as fatal or non-fatal

These fields are inherited by child `chain` when it is cloned.

In addition, `chain` maintains a reference to its parent and flags indicating whether a failure happened on the `chain` or any of its children.

When success or failure is reported, the following happens:

* `chain` invokes `AssertionHandler` and passes `AssertionContext` and `AssertionFailure` to it
* in case of failure, `chain` raises a flag on itself, indicating failure
* in case of failure, `chain` raises a flag on its parents and garndparents (up to the tree root), indicating that their children have failures (this feature is rarely used)

These failure flags are then used to ignore all subsequent assertions on a failed branch of the object tree. For example, if you run this code:

```go
e.GET("/test").Expect().Status(http.StatusOK).Body().IsObject()
```

and if `Status()` assertion failed, then this branch of the tree will be marked as failed, and calls to `Body()` and `IsObject()` will be just ignored. This is achieved by inheriting failure flag when cloning `chain`, and checking this flag in every assertion.

### Failure reporting

`AssertionHandler` is an interface that is used to handle every succeeded or failed assertion (like `IsEqual`).

It can be implemented by user if the user needs very precise control on assertion handling. In most cases, however, user does not need it, and just uses `DefaultAssertionHandler` implementation, which does the following:

* pass `AssertionContext` and `AssertionFailure` to `Formatter`, to get formatted message
* when reporting failed assertion, pass formatted message to `Reporter`
* when reporting succeeded assertion, pass formatted message to `Logger`

`Formatter`, `Reporter`, and `Logger` are also interfaces also can be implemented by user. Again, in most cases user can use default implemention for them:

* `DefaultFormatter` for `Formatter`
* `testing.T`, `FatalReporter`, `AssertReporter`, or `RequireReporter` for `Reporter`
* `testing.T` for `Logger`

In most cases, all the user needs to do is to select which reporter to use: `testing.T` or `FatalReporter` for non-fatal and fatal failure reports using standard `testing` package, and `AssertReporter` or `RequireReporter` for non-fatal and fatal failure reports using `testify` package (which adds nice backtrace and indentation).

For everything else, we will automatically employ default implementations (`DefaultFormatter`, `DefaultAssertionHandler`).

Note that `Formatter`, `Reporter`, and `Logger` are used only by `DefaultAssertionHandler`. If the user provides custom `AssertionHandler`, that implementation is free to ignore these three interfaces and can do whatever it wants.

## Code style

### Comment formatting

Exported functions should have documentation comments, formatted as follows:

Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,6 @@ ifneq ($(shell which mdspell),)
mdspell -a README.md
sort .spelling -o .spelling
endif

toc:
markdown-toc --maxdepth 3 -i HACKING.md

0 comments on commit d159043

Please sign in to comment.