From 3f32b8f6944ed679b3967cb37967fb270ab93084 Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Thu, 25 Apr 2024 23:18:38 +0200 Subject: [PATCH 1/5] docs: write unit testing guide --- docs/ocis/development/testing.md | 4 +- docs/ocis/development/unit-testing/_index.md | 85 ++++++ .../unit-testing/testing-ginkgo.md | 282 ++++++++++++++++++ .../development/unit-testing/testing-pkg.md | 114 +++++++ 4 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 docs/ocis/development/unit-testing/_index.md create mode 100644 docs/ocis/development/unit-testing/testing-ginkgo.md create mode 100644 docs/ocis/development/unit-testing/testing-pkg.md diff --git a/docs/ocis/development/testing.md b/docs/ocis/development/testing.md index ee5b9f72d3..4fe6737d85 100644 --- a/docs/ocis/development/testing.md +++ b/docs/ocis/development/testing.md @@ -1,7 +1,7 @@ --- -title: "Testing" +title: "Acceptance Testing" date: 2018-05-02T00:00:00+00:00 -weight: 37 +weight: 38 geekdocRepo: https://github.com/owncloud/ocis geekdocEditPath: edit/master/docs/ocis/development geekdocFilePath: testing.md diff --git a/docs/ocis/development/unit-testing/_index.md b/docs/ocis/development/unit-testing/_index.md new file mode 100644 index 0000000000..e1a0669fa5 --- /dev/null +++ b/docs/ocis/development/unit-testing/_index.md @@ -0,0 +1,85 @@ +--- +title: "Unit Testing" +date: 2024-04-25T00:00:00+00:00 +weight: 37 +geekdocRepo: https://github.com/owncloud/ocis +geekdocEditPath: edit/master/docs/ocis/development/unit-testing +geekdocFilePath: _index.md +--- + +{{< toc >}} + +Go is a statically typed language, which makes it easy to write unit tests. The Go standard library provides a `testing` package that allows you to write tests for your code. The testing package provides a framework for writing tests, and the `go test` command runs the tests. Other than that there are a lot of libraries and tools available to make testing easier. + +- [Testify](https://github.com/stretchr/testify) - A toolkit with common assertions and mocks that plays nicely with the standard library. +- [Ginkgo](https://onsi.github.io/ginkgo/) - A BDD-style testing framework for Go. +- [Gomega](https://onsi.github.io/gomega/) - A matcher/assertion library for Ginkgo. +- [GoDog](https://github.com/cucumber/godog) - A Behavior-Driven Development framework for Go which uses Gherkin. + +In ocis we generally use [Ginkgo](https://onsi.github.io/ginkgo/) framework for testing. To keep thins consistent, we would encourage you to use the same. In some cases, where you feel the need for a more verbose or more "code oriented" approach, you can also use the testing package from the standard library without ginkgo. + +## 1 Ginkgo + +Using a framework like [Ginkgo](https://onsi.github.io/ginkgo/) brings many advantages. + +### Pros + +- Provides a BDD-style syntax which makes it easier to write reusable and understandable tests +- Together with [Gomega](https://onsi.github.io/gomega/) it provides a powerful and expressive framework with assertions in a natural language +- Natural Language Format empowers testing in a way that resembles user interactions with the system +- In the context of microservices it is particularly well suited to test individual services and the interactions between them +- Offers support for asynchronous testing which makes it easier to test code that involves concurrency +- Nested and structured containers and setup capabilities make it easy to organize tests and adhere to the DRY principle +- Provides helpful error messages to identify and fix issues +- Very usable for Test Driven Development following the ["Red, Green, Cleanup, Repeat"](https://en.wikipedia.org/wiki/Test-driven_development) workflow. + +### Cons + +- Sometimes it can be difficult to get started with + +### Example + +As you can see, **Ginkgo** and **Gomega** together provide the foundation to write understandable and maintainable tests which can mimic user interaction and the interactions between microservices. + +```go +Describe("Public Share Provider", func() { + Context("When the user has no share permission", func() { + BeforeEach(func() { + // downgrade user permissions to have no share permission + resourcePermissions.AddGrant = false + }) + It("should return grpc invalid argument", func() { + req := &link.CreatePublicShareRequest{} + + res, err := provider.CreatePublicShare(ctx, req) + Expect(err).ToNot(HaveOccurred()) + Expect(res.GetStatus().GetCode()).To(Equal(rpc.Code_CODE_INVALID_ARGUMENT)) + Expect(res.GetStatus().GetMessage()).To(Equal("no share permission")) + }) +}) +``` + +### How to use it in ocis + +{{< button relref="testing-ginkgo" size="large" >}}{{< icon "gdoc_arrow_right_alt" >}} Read more{{< /button >}} + +## 2 Testing Package + +For smaller straight-forward tests of some packages it might feel more natural to use the testing package that comes with the go standard library. + +### Pros + +- Straightforward approach +- Naming conventions +- Built-in tooling + +### Cons + +- Difficult to reuse code in larger and more complex packages +- Difficult to create clean and isolated setups for the test steps +- No natural language resemblance + + +### How to use it in ocis + +{{< button relref="testing-pkg" size="large" >}}{{< icon "gdoc_arrow_right_alt" >}} Read more{{< /button >}} diff --git a/docs/ocis/development/unit-testing/testing-ginkgo.md b/docs/ocis/development/unit-testing/testing-ginkgo.md new file mode 100644 index 0000000000..74f3b503f8 --- /dev/null +++ b/docs/ocis/development/unit-testing/testing-ginkgo.md @@ -0,0 +1,282 @@ +--- +title: "Testing with Ginkgo" +date: 2024-04-25T00:00:00+00:00 +weight: 37 +geekdocRepo: https://github.com/owncloud/ocis +geekdocEditPath: edit/master/docs/ocis/development/unit-testing +geekdocFilePath: testing-ginkgo.md + +--- + +{{< toc >}} + +In this section we try to enable developers to write tests in ocis using Ginkgo and Gomega and explain how to mock other microservices to also cover some integration tests. The full documentation of the tools can be found on the [Ginkgo](https://onsi.github.io/ginkgo/) and [Gomega](https://onsi.github.io/gomega/) websites. + +{{% hint type=tip icon=gdoc_link title="Reading the documentation" %}} +This page provides only a basic introduction to get started with Ginkgo and Gomega. For more detailed information, please refer to the official documentation. + +**Useful Links:** + +- [Ginkgo](https://onsi.github.io/ginkgo/) +- [Gomega](https://onsi.github.io/gomega/) +- [Mockery](https://vektra.github.io/mockery/latest/) + +{{% /hint %}} + +## Prerequisites + +To use Ginkgo, you need to install the Ginkgo CLI. You can install it using the following command: + +```bash +go install github.com/onsi/ginkgo/v2/ginkgo +go get github.com/onsi/gomega/... +``` + +## Getting Started + +Navigate to the directory where you want to write your tests and run the following command: + +### Bootstrap + +```bash +cd ocis/ocis-pkg/config/parser +ginkgo bootstrap +Generating ginkgo test suite bootstrap for parser in: + parser_suite_test.go + +``` + +This command creates a file a `parser_suite_test.go` file in the parser directory. This file contains the test suite for the parser package. + +```go +package parser_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestParser(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Parser Suite") +} +``` + +Ginkgo defaults to setting up the suite as a `*_test` package to encourage you to only test the external behavior of your package, not its internal implementation details. + +After the package `parser_test` declaration we import the ginkgo and gomega packages into the test's top-level namespace by performing a `.` dot-import. Since Ginkgo and Gomega are DSLs (Domain-specific Languages) this makes the tests more natural to read. If you prefer, you can avoid the dot-import via `ginkgo bootstrap --nodot`. Throughout this documentation we'll assume dot-imports. + +With the bootstrap complete, you can now run your tests using the `ginkgo` command: + +```bash +ginkgo + +Running Suite: Parser Suite - /ocis/ocis-pkg/config/parser +=============================================================================================== +Random Seed: 1714076559 + +Will run 0 of 0 specs + +Ran 0 of 0 Specs in 0.000 seconds +SUCCESS! -- 0 Passed | 0 Failed | 0 Pending | 0 Skipped +PASS + +Ginkgo ran 1 suite in 7.0058606s +Test Suite Passed +``` + +Under the hood, ginkgo is simply calling `go test`. While you can run `go test` instead of the ginkgo CLI, Ginkgo has several capabilities that can only be accessed via `ginkgo`. We generally recommend users embrace the ginkgo CLI and treat it as a first-class member of their testing toolchain. + +### Adding Specs to the Suite + +```bash +ginkgo generate parser +Generating ginkgo test for Parser in:  ✔  7s  22:22:46  + parser_test.go +``` + +This will generate a `parser_test.go` file in the parser directory. This file contains the test suite for the parser package. + +```go +package parser_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/owncloud/ocis/v2/ocis-pkg/config/parser" +) + +var _ = Describe("Parser", func() { + +}) +``` + +## Writing Specs + +### Describe + +The `Describe` block is used to describe the behavior of a particular component of your code. It is a way to group together related specs. The `Describe` block takes a string and a function. The string is a description of the component you are describing, and the function contains the specs that describe the behavior of that component. + +```go +var _ = Describe("Parser", func() { + // Specs go here +}) +``` + +### Context + +The `Context` block is used to further describe the behavior of a component. It is a way to group together related specs within a `Describe` block. The `Context` block takes a string and a function. The string is a description of the context you are describing, and the function contains the specs that describe the behavior of that context. + +```go +var _ = Describe("Parser", func() { + Context("when the input is valid", func() { + // Specs go here + }) +}) +``` + +### It + +The `It` block is used to describe a single spec. It takes a string and a function. The string is a description of the behavior you are specifying, and the function contains the code that exercises that behavior. + +```go +var _ = Describe("Parser", func() { + Context("when the input is valid", func() { + It("parses the input", func() { + // Spec code goes here + }) + }) +}) +``` + +### Expect + +The `Expect` function is used to make assertions in your specs. It takes a value and returns an `*Expectation`. You can then chain methods on the `*Expectation` to make assertions about the value. + +```go +var _ = Describe("Parser", func() { + Context("when the input is valid", func() { + It("parses the input", func() { + result := parser.Parse("valid input") + Expect(result).To(Equal("expected output")) + }) + }) +}) +``` + +### BeforeEach + +The `BeforeEach` block is used to run a setup function before each spec in a `Describe` or `Context` block. It takes a function that contains the setup code. + +```go +package parser_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/owncloud/ocis/v2/ocis-pkg/config" + + p "github.com/owncloud/ocis/v2/ocis-pkg/config/parser" +) + +var _ = Describe("Parser", func() { + var c *config.Config + + BeforeEach(func() { + c = config.DefaultConfig() + }) + + Context("when the input is valid", func() { + It("parses the input", func() { + err := p.ParseConfig(c, false) + Expect(err).ToNot(HaveOccurred()) + Expect(c.Commons.OcisURL).To(Equal("https://localhost:9200")) + }) + }) +}) +``` + +Let us take a closer look at the code above: + +We are following the recommended practise on variables to **"declare in container nodes"** and **"initialize in setup nodes"**. This is why we are declaring the `c` variable at the top of the `Describe` block and initializing it in the `BeforeEach` block. This is important to get isolated test steps which can be run in any order and even in parallel. + +Let us take a look at a bad example where we are polluting the spec by not following this recommended practise: + +```go +package parser_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/owncloud/ocis/v2/ocis-pkg/config" + + p "github.com/owncloud/ocis/v2/ocis-pkg/config/parser" +) + + +var _ = Describe("Parser", func() { + c := config.DefaultConfig() + + Context("when the defaults are applied", func() { + It("fails to parse the input", func() { + c.TokenManager.JWTSecret = "" // bam! we have changed the closure variable and it will never be reset + err := p.ParseConfig(c, false) + Expect(err).To(HaveOccurred()) + }) + It("parses the input", func() { + err := p.ParseConfig(c, false) + Expect(err).ToNot(HaveOccurred()) + Expect(c.Commons.OcisURL).To(Equal("https://localhost:9200")) + }) + }) +}) +``` + +{{% hint type="warning" title="Specs MUST be clean and independent"%}} +Always **declare variables in the container node**(which are basically `Describe()` and `Context()`) + +and **initialize your variables in the setup nodes.** (which are basically `BeforeEach()` and `JustBeforeEach()`). + +This will ensure that your specs are clean and independent of each other. +{{% /hint %}} + +### Focused Specs + +You can focus on a single spec by adding an `F` in front of the `It` block. This will run only the focused spec. + +```go +var _ = Describe("Parser", func() { + Context("when the input is valid", func() { + FIt("parses the input", func() { + result := parser.Parse("valid input") + Expect(result).To(Equal("expected output")) + }) + }) +}) +``` + +### Pending Specs + +You can mark a spec as pending by adding a `P` in front of the `It` block. This will skip the spec. + +```go +var _ = Describe("Parser", func() { + Context("when the input is valid", func() { + PIt("parses the input", func() { + result := parser.Parse("valid input") + Expect(result).To(Equal("expected output")) + }) + }) +}) +``` + +### Test Driven Development + +You can run the tests in watch mode to follow a test-driven development approach. This will run the tests every time you save a file. + +```bash +ginkgo watch +``` diff --git a/docs/ocis/development/unit-testing/testing-pkg.md b/docs/ocis/development/unit-testing/testing-pkg.md new file mode 100644 index 0000000000..88f59ecef9 --- /dev/null +++ b/docs/ocis/development/unit-testing/testing-pkg.md @@ -0,0 +1,114 @@ +--- +title: "Standard Library Testing" +date: 2024-04-25T00:00:00+00:00 +weight: 37 +geekdocRepo: https://github.com/owncloud/ocis +geekdocEditPath: edit/master/docs/ocis/development/unit-testing +geekdocFilePath: testing-pkg.md + +--- + +## Using the standard library + +To write a unit test for your package, create a file with the `_test.go` suffix. For example, if you have a package `foo` with a file `foo.go`, you can create a file `foo_test.go` in the same directory. The test file should have the same package name as the package being tested. By doing this, you can access all exported and unexported identifiers of the package. It is a good practice to keep the test file in the same package as the code being tested. + +### Simple Example + +We are using an oversimplified example from [FooBarQuix](https://codingdojo.org/kata/FooBarQix/) to demonstrate how to use the `testing` package. + +```go +package divide + +import "strconv" + +// If the number is divisible by 3, write "Yes" otherwise, the number +func IsDevisible(input int) string { + isDevisible:= (input % 3) == 0 + if isDevisible { + return "Yes" + } + return strconv.Itoa(input) +} +``` + +To test the `IsDevisible` function, create a file `divide_test.go` in the same directory as `divide.go`. The test file should have the same package name as the package being tested. + +A test function in Go starts with `Test` and takes `*testing.T` as the only parameter. In most cases, you will name the unit test `Test[NameOfFunction]`. The testing package provides tools to interact with the test workflow, such as `t.Errorf`, which indicates that the test failed by displaying an error message on the console. + +The test function for the `IsDevisible` function could look like this + +```go +package divide + +import "testing" + +func TestDevide3(t *testing.T) { + result := IsDevisible(3) + if result != "Yes" { + t.Errorf("Result was incorrect, got: %s, want: %s.", result, "Foo") + } +} +``` + +To run the test, use the `go test` command in the directory where the test file is located. + +### Use a helper package for assertions + +You could make the test more readable by using testify. The `assert` package provides a lot of helper functions to make the test more readable. + +```go +package divide + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestDevide3(t *testing.T) { + result := IsDevisible(3) + assert.Equal(t, "Yes", result) +} +``` + +### Table Driven Example + +Write Table Drive Tests to test multiple inputs. + +```go +package divide + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + + +func TestIsDevisibleTableDriven(t *testing.T) { + // Defining the columns of the table + var tests = []struct { + name string + input int + want string + }{ + // the table itself + {"9 should be Yes", 9, "Yes"}, + {"3 should be Yes", 3, "Yes"}, + {"1 is not Yes", 1, "1"}, + {"0 should be Yes", 0, "Yes"}, + } + + // The execution loop + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + answer := IsDevisible(tt.input) + assert.Equal(t, tt.want, answer) + }) + } +} +``` + +A table-driven test starts by defining the input structure. This can be seen like defining the columns of the table. Each row of the table lists a test case to execute. Once the table is defined, the execution loop can be created. + +The execution loop calls `t.Run()`, which defines a subtest. In our example each row of the table defines a subtest named `[NameOfTheFuction]/[NameOfTheSubTest]`. + +This way of writing tests is very popular, and considered the canonical way to write unit tests in Go. From 54883c769f625324f3ac863b62dac83f61bc5d7b Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Mon, 29 Apr 2024 11:25:55 +0200 Subject: [PATCH 2/5] Apply suggestions from code review Co-authored-by: Phil Davis Co-authored-by: kobergj --- .../unit-testing/testing-ginkgo.md | 2 +- .../development/unit-testing/testing-pkg.md | 23 +++++++++---------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/ocis/development/unit-testing/testing-ginkgo.md b/docs/ocis/development/unit-testing/testing-ginkgo.md index 74f3b503f8..0264c1169a 100644 --- a/docs/ocis/development/unit-testing/testing-ginkgo.md +++ b/docs/ocis/development/unit-testing/testing-ginkgo.md @@ -46,7 +46,7 @@ Generating ginkgo test suite bootstrap for parser in: ``` -This command creates a file a `parser_suite_test.go` file in the parser directory. This file contains the test suite for the parser package. +This command creates a `parser_suite_test.go` file in the parser directory. This file contains the test suite for the parser package. ```go package parser_test diff --git a/docs/ocis/development/unit-testing/testing-pkg.md b/docs/ocis/development/unit-testing/testing-pkg.md index 88f59ecef9..65d0add1ca 100644 --- a/docs/ocis/development/unit-testing/testing-pkg.md +++ b/docs/ocis/development/unit-testing/testing-pkg.md @@ -22,28 +22,27 @@ package divide import "strconv" // If the number is divisible by 3, write "Yes" otherwise, the number -func IsDevisible(input int) string { - isDevisible:= (input % 3) == 0 - if isDevisible { +func IsDivisible(input int) string { + if (input % 3) == 0 { return "Yes" } return strconv.Itoa(input) } ``` -To test the `IsDevisible` function, create a file `divide_test.go` in the same directory as `divide.go`. The test file should have the same package name as the package being tested. +To test the `IsDivisible` function, create a file `divide_test.go` in the same directory as `divide.go`. The test file should have the same package name as the package being tested. A test function in Go starts with `Test` and takes `*testing.T` as the only parameter. In most cases, you will name the unit test `Test[NameOfFunction]`. The testing package provides tools to interact with the test workflow, such as `t.Errorf`, which indicates that the test failed by displaying an error message on the console. -The test function for the `IsDevisible` function could look like this +The test function for the `IsDivisible` function could look like this ```go package divide import "testing" -func TestDevide3(t *testing.T) { - result := IsDevisible(3) +func TestDivide3(t *testing.T) { + result := IsDivisible(3) if result != "Yes" { t.Errorf("Result was incorrect, got: %s, want: %s.", result, "Foo") } @@ -64,15 +63,15 @@ import ( "github.com/stretchr/testify/assert" ) -func TestDevide3(t *testing.T) { - result := IsDevisible(3) +func TestDivide3(t *testing.T) { + result := IsDivisible(3) assert.Equal(t, "Yes", result) } ``` ### Table Driven Example -Write Table Drive Tests to test multiple inputs. +Write Table Driven Tests to test multiple inputs. ```go package divide @@ -83,7 +82,7 @@ import ( ) -func TestIsDevisibleTableDriven(t *testing.T) { +func TestIsDivisibleTableDriven(t *testing.T) { // Defining the columns of the table var tests = []struct { name string @@ -100,7 +99,7 @@ func TestIsDevisibleTableDriven(t *testing.T) { // The execution loop for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - answer := IsDevisible(tt.input) + answer := IsDivisible(tt.input) assert.Equal(t, tt.want, answer) }) } From 5ad77f7dee7242e668d69388f14e00af396aa14e Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Thu, 25 Apr 2024 23:18:38 +0200 Subject: [PATCH 3/5] docs: add mocks to testing guide --- docs/ocis/development/unit-testing/_index.md | 6 +- .../unit-testing/testing-ginkgo.md | 117 +++++++++++++++++- .../development/unit-testing/testing-pkg.md | 2 +- 3 files changed, 119 insertions(+), 6 deletions(-) diff --git a/docs/ocis/development/unit-testing/_index.md b/docs/ocis/development/unit-testing/_index.md index e1a0669fa5..d51ce2e83f 100644 --- a/docs/ocis/development/unit-testing/_index.md +++ b/docs/ocis/development/unit-testing/_index.md @@ -1,7 +1,7 @@ --- title: "Unit Testing" date: 2024-04-25T00:00:00+00:00 -weight: 37 +weight: 5 geekdocRepo: https://github.com/owncloud/ocis geekdocEditPath: edit/master/docs/ocis/development/unit-testing geekdocFilePath: _index.md @@ -16,7 +16,7 @@ Go is a statically typed language, which makes it easy to write unit tests. The - [Gomega](https://onsi.github.io/gomega/) - A matcher/assertion library for Ginkgo. - [GoDog](https://github.com/cucumber/godog) - A Behavior-Driven Development framework for Go which uses Gherkin. -In ocis we generally use [Ginkgo](https://onsi.github.io/ginkgo/) framework for testing. To keep thins consistent, we would encourage you to use the same. In some cases, where you feel the need for a more verbose or more "code oriented" approach, you can also use the testing package from the standard library without ginkgo. +In oCIS we generally use [Ginkgo](https://onsi.github.io/ginkgo/) framework for testing. To keep things consistent, we would encourage you to use the same. In some cases, where you feel the need for a more verbose or more "code oriented" approach, you can also use the testing package from the standard library without ginkgo. ## 1 Ginkgo @@ -59,7 +59,7 @@ Describe("Public Share Provider", func() { }) ``` -### How to use it in ocis +### How to use it in oCIS {{< button relref="testing-ginkgo" size="large" >}}{{< icon "gdoc_arrow_right_alt" >}} Read more{{< /button >}} diff --git a/docs/ocis/development/unit-testing/testing-ginkgo.md b/docs/ocis/development/unit-testing/testing-ginkgo.md index 0264c1169a..b4357566ec 100644 --- a/docs/ocis/development/unit-testing/testing-ginkgo.md +++ b/docs/ocis/development/unit-testing/testing-ginkgo.md @@ -1,7 +1,7 @@ --- title: "Testing with Ginkgo" date: 2024-04-25T00:00:00+00:00 -weight: 37 +weight: 10 geekdocRepo: https://github.com/owncloud/ocis geekdocEditPath: edit/master/docs/ocis/development/unit-testing geekdocFilePath: testing-ginkgo.md @@ -10,7 +10,7 @@ geekdocFilePath: testing-ginkgo.md {{< toc >}} -In this section we try to enable developers to write tests in ocis using Ginkgo and Gomega and explain how to mock other microservices to also cover some integration tests. The full documentation of the tools can be found on the [Ginkgo](https://onsi.github.io/ginkgo/) and [Gomega](https://onsi.github.io/gomega/) websites. +In this section we try to enable developers to write tests in oCIS using Ginkgo and Gomega and explain how to mock other microservices to also cover some integration tests. The full documentation of the tools can be found on the [Ginkgo](https://onsi.github.io/ginkgo/) and [Gomega](https://onsi.github.io/gomega/) websites. {{% hint type=tip icon=gdoc_link title="Reading the documentation" %}} This page provides only a basic introduction to get started with Ginkgo and Gomega. For more detailed information, please refer to the official documentation. @@ -280,3 +280,116 @@ You can run the tests in watch mode to follow a test-driven development approach ```bash ginkgo watch ``` + +## Mocking + +In oCIS, we use the `mockery` tool to generate mocks for interfaces. [Mockery](https://vektra.github.io/mockery/latest/) is a simple tool that generates mock implementations of Go interfaces. It is useful for writing tests against interfaces instead of concrete types. We can use it to mock requests to other microservices to cover some integration tests. We should already have a number of mocks in the project. The mocks are configured on the packages level in the `.mockery.yaml` files. + +**Example file:** + +```yaml +with-expecter: true +filename: "{{.InterfaceName | snakecase }}.go" +dir: "{{.PackageName}}/mocks" +mockname: "{{.InterfaceName}}" +outpkg: "mocks" +packages: + github.com/owncloud/ocis/v2/ocis-pkg/oidc: + interfaces: + OIDCClient: +``` + +We should add missing mocks to this file and define the interfaces we want to mock. After that, we can generate the mocks by running `mockery` in the repo, it will find all the `.mockery.yaml` files and generate the mocks for the interfaces defined in them. + +Our mocks are generated with the setting `with-expecter: true`. This allows us to use type-safe methods to generate the call expectations by simply calling `EXPECT()` on the mock object. + +{{% hint type="tip" title="Type safe mock identifiers" %}} +By using `EXPECT()` on the mock object, we can work with type-safe methods to generate the call expectations. +{{% /hint %}} + +**Example of a mocked gateway client** + +In our oCIS services we need to use a gateway pool selector to get the gateway client. + +We should always use the constructor on a new mock like `gatewayClient = cs3mocks.NewGatewayAPIClient(GinkgoT())`. This brings us two advantages: + +- The `AssertExpectations` method is registered to be called at the end of the tests via `t.Cleanup()` method. +- The `testing.TB` interface is registered on the `mock.Mock` so that tests don't panic when a call on the mock is unexpected. + +```go +package publicshareprovider_test + +import ( + "context" + "time" + + + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "google.golang.org/grpc" +) + +var _ = Describe("PublicShareProvider", func() { + // declare in container nodes + var ( + gatewayClient *cs3mocks.GatewayAPIClient + gatewaySelector pool.Selector + ) + + BeforeEach(func() { + // initialize in setup nodes + pool.RemoveSelector("GatewaySelector" + "any") + // create a new mock client + gatewayClient = cs3mocks.NewGatewayAPIClient(GinkgoT()) + gatewaySelector = pool.GetSelector[gateway.GatewayAPIClient]( + "GatewaySelector", + "any", + func(cc *grpc.ClientConn) gateway.GatewayAPIClient { + return gatewayClient + }, + ) + }) + Context("The user has the permission to create public shares", func() { + BeforeEeach(func() { + // set up the mock + // this is implicitly creating the expectation that it will be called Once() + // this will throw an error if the method is not called + gatewayClient. + EXPECT(). + CheckPermission( + mock.Anything, + mock.Anything, + ). + Return(checkPermissionResponse, nil) + }) + It("should return a public share", func() { + // call the method + req := &link.CreatePublicShareRequest{ + ResourceInfo: &providerpb.ResourceInfo{ + Owner: &userpb.UserId{ + OpaqueId: "alice", + }, + Path: "./NewFolder/file.txt", + }, + Grant: &link.Grant{ + Permissions: &link.PublicSharePermissions{ + Permissions: linkPermissions, + }, + Password: "SecretPassw0rd!", + }, + Description: "test", + } + res, err := provider.CreatePublicShare(ctx, req) + Expect(err).ToNot(HaveOccurred()) + Expect(res.GetStatus().GetCode()).To(Equal(rpc.Code_CODE_OK)) + Expect(res.GetShare()).To(Equal(createdLink)) + }) + }) +}) +``` + +{{% hint type="tip" title="Mocking in oCIS" %}} +Use the constructor on new mocks to register the `AssertExpectations` method to be called at the end of the tests via the `t.Cleanup()` method. +{{% /hint %}} diff --git a/docs/ocis/development/unit-testing/testing-pkg.md b/docs/ocis/development/unit-testing/testing-pkg.md index 65d0add1ca..1d39d5efe8 100644 --- a/docs/ocis/development/unit-testing/testing-pkg.md +++ b/docs/ocis/development/unit-testing/testing-pkg.md @@ -1,7 +1,7 @@ --- title: "Standard Library Testing" date: 2024-04-25T00:00:00+00:00 -weight: 37 +weight: 15 geekdocRepo: https://github.com/owncloud/ocis geekdocEditPath: edit/master/docs/ocis/development/unit-testing geekdocFilePath: testing-pkg.md From 5ae4d5646b160459b6364a3c4707160af4647c44 Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Mon, 29 Apr 2024 12:04:05 +0200 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Phil Davis --- docs/ocis/development/unit-testing/testing-ginkgo.md | 2 +- docs/ocis/development/unit-testing/testing-pkg.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ocis/development/unit-testing/testing-ginkgo.md b/docs/ocis/development/unit-testing/testing-ginkgo.md index b4357566ec..1617bdd070 100644 --- a/docs/ocis/development/unit-testing/testing-ginkgo.md +++ b/docs/ocis/development/unit-testing/testing-ginkgo.md @@ -73,7 +73,7 @@ With the bootstrap complete, you can now run your tests using the `ginkgo` comma ```bash ginkgo -Running Suite: Parser Suite - /ocis/ocis-pkg/config/parser +Running Suite: Parser Suite - /ocis/ocis-pkg/config/parser =============================================================================================== Random Seed: 1714076559 diff --git a/docs/ocis/development/unit-testing/testing-pkg.md b/docs/ocis/development/unit-testing/testing-pkg.md index 1d39d5efe8..8c5161005c 100644 --- a/docs/ocis/development/unit-testing/testing-pkg.md +++ b/docs/ocis/development/unit-testing/testing-pkg.md @@ -44,7 +44,7 @@ import "testing" func TestDivide3(t *testing.T) { result := IsDivisible(3) if result != "Yes" { - t.Errorf("Result was incorrect, got: %s, want: %s.", result, "Foo") + t.Errorf("Result was incorrect, got: %s, want: %s.", result, "Yes") } } ``` From 0dce02d011950f7e0770a476e666d2f836a9f4a4 Mon Sep 17 00:00:00 2001 From: Michael Barz Date: Mon, 29 Apr 2024 12:06:29 +0200 Subject: [PATCH 5/5] docs: add more cons from code review --- docs/ocis/development/unit-testing/_index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ocis/development/unit-testing/_index.md b/docs/ocis/development/unit-testing/_index.md index d51ce2e83f..952819d3e4 100644 --- a/docs/ocis/development/unit-testing/_index.md +++ b/docs/ocis/development/unit-testing/_index.md @@ -36,6 +36,8 @@ Using a framework like [Ginkgo](https://onsi.github.io/ginkgo/) brings many adva ### Cons - Sometimes it can be difficult to get started with +- Asynchronous behaviour brings more complexity to tests. +- Not compatible with broadly known `testify` package ### Example