-
Notifications
You must be signed in to change notification settings - Fork 2
Add API functional tests, describe the testing process #5
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
Open
m-zajac
wants to merge
4
commits into
nglogic:master
Choose a base branch
from
m-zajac:testing
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,7 +38,6 @@ linters: | |
| - revive | ||
| - stylecheck | ||
| - thelper | ||
| - tparallel | ||
| - unconvert | ||
| - unparam | ||
| - wastedassign | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| # Testing | ||
|
|
||
| Good testing is critical for any project. In this article, we won't focus on unit testing or test coverage. I'll try to explain the important parts to test and how to do it easily. | ||
|
|
||
| ## How good architecture helps with good testing | ||
|
|
||
| Following clean architecture principles, we've decoupled the application, data access, and API layers. So now, we have contracts between them in the form of interfaces. | ||
| What it means for testing is that now for each layer, we can substitute its dependencies with mocks. Therefore, we can test each layer in separation. Remember, interfaces can serve as mocking points! | ||
|
|
||
| In the example application tests, we test all the components as a whole, focusing on functional testing. But still, we use this property to mock external services that we can't use for testing. | ||
|
|
||
| ## Testing the example app | ||
|
|
||
| There are a lot of articles about testing in Go. But in this guide, I want to focus on what's crucial - **functional testing**. | ||
|
|
||
| Why start with functional testing? | ||
|
|
||
| 1. API is the contract between our code and the user. Therefore, any unwanted change to that contract could result in failures on our client-side. Moreover, by introducing unwanted API behavior, we could not be aware that our client code is failing - we won't get any alerts or errors in the logs. | ||
| 2. Your code will evolve over time, but probably existing API endpoints will stay more or less the same. Therefore, writing tests based on the API will reduce the maintenance burden. | ||
| Whenever you change the internal workings of the application, API level tests should work fine without any changes. | ||
| Even more importantly, such tests would be a **huge** help in case of some heavy refactoring in the future. | ||
|
|
||
| So we want to set up the test suite to cover as much of the code as possible using functional tests. The following diagram shows how the functional test case is going to look like: | ||
|
|
||
|  | ||
|
|
||
| For each functional test, we're going to prepare an API, listening on an actual network socket. It would be set up in the same way as in `cmd/app`, but with the test database and external service mocks. And in each test, we're going to create an actual GRPC client, make some GRPC requests and check the responses. | ||
|
|
||
| ### Dealing with dependencies | ||
|
|
||
| We have three external systems that our service needs to work: database, weather service, and bike incident service. There's no way to create actual, working weather or bike incident services. So for those, we'll use mocks. But we can create a local `Postgres` database using `Docker` and we're going to use `ory/dockertest` library for that. | ||
|
|
||
| In general, I advise testing with actual databases/services whenever feasible. | ||
|
|
||
| For database I created a test helper function, that creates a `Postgres` container, and returns connection data - `internal/test/db.go, startDB`. For other services, I create mocks in each test case using `gomock`. | ||
|
|
||
| *Hint: To create mocks, I use `github.com/golang/mock/gomock` and `github.com/golang/mock/mockgen` tool. Check `Makefile` and `go:generate` commands in go files for more details.* | ||
|
|
||
| If you want to know more about using and testing with Postgres in go, check out this great video: [GopherCon 2020: Johan Brandhorst-Satzkorn - A Journey to Postgres Productivity with Go](https://www.youtube.com/watch?v=AgHdVPSty7k) | ||
|
|
||
| ### Testing API | ||
|
|
||
| We have to have some helper function to set up the whole API for the test (this big container named "Rental API" in the diagram above). It's implemented in: `internal/test/testsetup.go, newFunctionalTestSetup`. `newFunctionalTestSetup` creates all the app components and composes them together into a fully functional service. Then it creates GRPC server that exposes this service, and the client that would be used for testing. | ||
|
|
||
| All "access points" to this whole system are available in the `testSetup` struct, that is the result of running `newFunctionalTestSetup`. Later in tests you will have access a GRPC client, and db adapters directly. | ||
|
|
||
| The pattern for API testing looks like this: | ||
|
|
||
| - Write a test for an API method. | ||
| - In the test create `Postgres` db container using `internal/test/db.go, startDB` helper. You will get a struct with the connection details. | ||
| - Then initiate an object with all the components bound together and start the GRPC server, using `newFunctionalTestSetup` function. | ||
| - If necessary, prepare test data using adapters and helper functions (check `TestListReservations` and `createReservation` helper function to see the example). | ||
| - Prepare a test request and send it to the server. | ||
| - Check the response against the expected result. | ||
|
|
||
| ### Separating "heavy" functional tests from "light" unit tests | ||
|
|
||
| It's useful to be able to run only the fast tests, skipping those that require dependencies like databases or external services. So, for example, I like to have a "fast test" set up as a git hook (together with linting). | ||
| It prevents me from committing broken code accidentally. | ||
|
|
||
| To do that, I propose adding a build flag to all the tests with external dependencies. In the example code, we use `func` build flag. And then in the `Makefile` you have to "helpers" to run tests: | ||
|
|
||
| - `make test` - will run all but the functional tests. | ||
| - `make test-all` - will run all the tests. | ||
|
|
||
| You can use these commands in the git hook scripts. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I would argue tests should be scenarios related to particular functionality of the application. In majority of cases, functionality tests would require usage of more API endpoints than just one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be the case for e2e tests, but I don't think it is for functional API testing. In this case we don't care about how for example the frontend app is using our API. I think that maybe naming is a bit confusing here? But I still think naming those test "functional" makes sense.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Firstly, let's cite the definitions.
(Assumption: e2e == system level.)
Source: https://en.wikipedia.org/wiki/System_testing
Source: https://en.wikipedia.org/wiki/Functional_testing
I believe I used term "functionality tests" properly: we would want to focus on testing functionalities as defined: "if the software component satisfies the specifications". I didn't mean to mention e2e, since this architecture has only one component, therefore e2e == integration tests (i.e. there is no frontend).
Regarding my original comment, I would just argue that guidelines for API level tests shouldn't focus on particular "API method", but instead on the tested functionality. You also outline the importance of focus on "functional testing". It is just a wording of the point in the guidelines that my original comment relates to.
Currently, at least I understood it this way, the guidelines propose to "write a test for an API method" which literally suggests to create separate test case per each endpoint (e.g. focus on validation of the request/response format and not outline the importance of testing the actual logic). In my opinion, we would rather want to outline the focus on testing functionalities and reflect such need in the guidelines. Even more, I believe your intent is to test the functionality as well (e.g.
TestUpdateBikefocuses on testing the functionality in order to check if overall logic of "Update" works properly). However, it doesn't use multiple endpoints which I will try to cover below.In my original comment, I assumed we would want to use multiple endpoints to test "scenarios". Why? In order to test the "Update"/"Delete" operation on Bike object, we would have to firstly create one. In your tests you use a database adapter to create Bikes (
createSpecificBike). I personally would rather implement such tests by callingv1.CreateBikeendpoint, but your approach is also fine given the current complexity of the system. However, if the system would grow, and we would add more logic to objects creation, then injecting the data directly to database from the test code (which would skip business logic) might lead to a state in the database which doesn't fully express what would happen if we have used a real endpoint. Therefore, for more complex systems (or ones which have the "real" potential to become one), I would rather use endpoints instead of database adapters to populate the data. In this particular case of Bike service, I guess it doesn't matter.To be honest, I might have been overly strict, because it sounds like in the end we would like to see (more or less) the same implementation of functional tests (which is actually expressed as a source code pretty clearly). Feel free to disregard this comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @mwarzynski, sorry for the late reply.
I argue that for functional testing you don't need to know the context of the app (how it is used in a system, or by an external client), so it doesn't really make sense to write usage scenarios. You are not the consumer of the API, so by writing such scenarios you can fool yourself that you know exactly how your client is using the API and falsely claim that you have proper API usage tests. And in a REST API, each endpoint defines specific functionality almost by definition (they usually expose atomic operations on resources, they usually are independent), so I think in this example it makes sense to focus functional tests on endpoints. But I agree I have to restate the guidelines, because that doesn't always have to be the case.
I agree that I created too much isolation of the endpoints in tests, I'll improve that.
I'll make a few changes and get back to you. I'm having a break from coding recently, but I hope I'll get back to it soon :)
Merry Christmas! :)