github.com/jonsyu1/godel@v0.0.0-20171017211503-64567a0cf169/docs/Integration-tests.md (about)

     1  Summary
     2  -------
     3  The `github.com/palantir/godel/pkg/products` package can be used to write integration tests that run against the build
     4  artifacts of a product and test tags can be used to define test sets for integration tests.
     5  
     6  Tutorial start state
     7  --------------------
     8  
     9  * `$GOPATH/src/github.com/nmiyake/echgo` exists and is the working directory
    10  * Project contains `godel` and `godelw`
    11  * Project contains `main.go`
    12  * Project contains `.gitignore` that ignores IDEA files
    13  * Project contains `echo/echo.go`, `echo/echo_test.go` and `echo/echoer.go`
    14  * `godel/config/dist.yml` is configured to build `echgo`
    15  * Project is tagged as 0.0.1
    16  * `godel/config/dist.yml` is configured to create distributions for `echgo`
    17  * Project is tagged as 0.0.2
    18  * Go files have license headers
    19  * `godel/config/generate.yml` is configured to generate string function
    20  * `godel/config/exclude.yml` is configured to ignore all `.+_string.go` files
    21  
    22  ([Link](https://github.com/nmiyake/echgo/tree/1982133dbe7c811f1e2d71f4dcc25ff20f84146a))
    23  
    24  Write tests that run using build artifacts
    25  ------------------------------------------
    26  
    27  `echgo` currently has unit tests that test the contracts of the `echgo` package. Unit tests are a great way to test the
    28  API contracts of packages, and in an ideal world all of the packages for a project having tests that verify the package
    29  APIs would be sufficient to ensure the correctness of an entire program.
    30  
    31  However, in many cases there exists behavior that can only be tested in a true end-to-end workflow. For example, `echgo`
    32  currently has some logic in its `main.go` file that parses the command-line flags, determines what functions to call
    33  based on flags and ultimately prints the output to the console. If we want to test things such as what happens when
    34  invalid values are supplied as flags, how multiple command-line arguments are parsed or the exit codes of the program,
    35  there is not a straightforward way to write that test.
    36  
    37  The `github.com/palantir/godel/pkg/products` packages provides functionality that makes it easy to write such tests for
    38  projects that use gödel to build their products. The `products` package provides functions that ensure that specified
    39  products are built using the build configuration defined for the product and provides a path to the built executable
    40  that can be used for testing.
    41  
    42  We need to add `github.com/palantir/godel/pkg/products` as a vendored dependency for the project. Start by cloning the
    43  gödel project:
    44  
    45  ```
    46  ➜ mkdir -p $GOPATH/github.com/palantir && cd $_
    47  ➜ git clone https://github.com/palantir/godel.git
    48  Cloning into 'godel'...
    49  remote: Counting objects: 3210, done.
    50  remote: Total 3210 (delta 0), reused 0 (delta 0), pack-reused 3209
    51  Receiving objects: 100% (3210/3210), 3.65 MiB | 1.92 MiB/s, done.
    52  Resolving deltas: 100% (1382/1382), done.
    53  Checking connectivity... done.
    54  ```
    55  
    56  There are multiple different ways to vendor dependencies. For the purposes of this tutorial, we will forego formal
    57  vendoring and vendor the dependency manually.
    58  
    59  ```
    60  ➜ cd $GOPATH/src/github.com/nmiyake/echgo
    61  ➜ mkdir -p vendor/github.com/palantir/godel/pkg/products
    62  ➜ cp $GOPATH/src/github.com/palantir/godel/pkg/products/* vendor/github.com/palantir/godel/pkg/products/
    63  ```
    64  
    65  Run the following to define a test that tests the behavior of invoking `echgo` with an invalid echo type and run the
    66  test (this test is still in the iteration phase, so it simply prints the result of the output rather than asserting
    67  against it):
    68  
    69  ```
    70  ➜ mkdir -p integration_test
    71  ➜ echo '// Copyright (c) 2017 Author Name
    72  //
    73  // Licensed under the Apache License, Version 2.0 (the "License");
    74  // you may not use this file except in compliance with the License.
    75  // You may obtain a copy of the License at
    76  //
    77  //     http://www.apache.org/licenses/LICENSE-2.0
    78  //
    79  // Unless required by applicable law or agreed to in writing, software
    80  // distributed under the License is distributed on an "AS IS" BASIS,
    81  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    82  // See the License for the specific language governing permissions and
    83  // limitations under the License.
    84  
    85  package integration_test
    86  
    87  import (
    88  	"fmt"
    89  	"os/exec"
    90  	"testing"
    91  
    92  	"github.com/palantir/godel/pkg/products"
    93  )
    94  
    95  func TestInvalidType(t *testing.T) {
    96  	echgoPath, err := products.Bin("echgo")
    97  	if err != nil {
    98  		panic(err)
    99  	}
   100  	cmd := exec.Command(echgoPath, "-type", "invalid", "foo")
   101  	output, err := cmd.CombinedOutput()
   102  	if err != nil {
   103  		t.Errorf("cmd %v failed with error %v. Output: %s", cmd.Args, err, string(output))
   104  	}
   105  	fmt.Printf("%q", string(output))
   106  	fmt.Println()
   107  }' > integration_test/integration_test.go
   108  ➜ go test -v ./integration_test
   109  === RUN   TestInvalidType
   110  "invalid echo type: invalid\n"
   111  --- PASS: TestInvalidType (1.43s)
   112  PASS
   113  ok  	github.com/nmiyake/echgo/integration_test	1.566s
   114  ```
   115  
   116  The `products.Bin("echgo")` call uses gödel to build the `echgo` product (if needed) and returns a path to the binary
   117  that was built. Because this is a path to a valid binary, `exec.Command` can be use to invoke it. This allows the test
   118  to specify arguments, hook up input/output streams, check error values and assert various behavior.
   119  
   120  In this case, the output seems reasonable -- it prints `invalid echo type: invalid\n`. However, note that the error was
   121  `nil` -- this is a bug. If the specified echo type was invalid, then the program should return with a non-zero exit
   122  code, which should cause `cmd.CombinedOutput` to return an error.
   123  
   124  Fix the bug by updating `main.go` and then re-run the test:
   125  
   126  ```
   127  ➜ echo '// Copyright (c) 2017 Author Name
   128  //
   129  // Licensed under the Apache License, Version 2.0 (the "License");
   130  // you may not use this file except in compliance with the License.
   131  // You may obtain a copy of the License at
   132  //
   133  //     http://www.apache.org/licenses/LICENSE-2.0
   134  //
   135  // Unless required by applicable law or agreed to in writing, software
   136  // distributed under the License is distributed on an "AS IS" BASIS,
   137  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   138  // See the License for the specific language governing permissions and
   139  // limitations under the License.
   140  
   141  package main
   142  
   143  import (
   144  	"flag"
   145  	"fmt"
   146  	"os"
   147  	"strings"
   148  
   149  	"github.com/nmiyake/echgo/echo"
   150  )
   151  
   152  var version = "none"
   153  
   154  func main() {
   155  	versionVar := flag.Bool("version", false, "print version")
   156  	typeVar := flag.String("type", echo.Simple.String(), "type of echo")
   157  	flag.Parse()
   158  	if *versionVar {
   159  		fmt.Println("echgo version:", version)
   160  		return
   161  	}
   162  	typ, err := echo.TypeFrom(*typeVar)
   163  	if err != nil {
   164  		fmt.Println("invalid echo type:", *typeVar)
   165  		os.Exit(1)
   166  	}
   167  	echoer := echo.NewEchoer(typ)
   168  	fmt.Println(echoer.Echo(strings.Join(flag.Args(), " ")))
   169  }' > main.go
   170  ➜ go test -v ./integration_test
   171  === RUN   TestInvalidType
   172  "invalid echo type: invalid\n"
   173  --- FAIL: TestInvalidType (1.38s)
   174  	integration_test.go:33: cmd [/Volumes/git/go/src/github.com/nmiyake/echgo/build/0.0.2-4-g1982133.dirty/darwin-amd64/echgo -type invalid foo] failed with error exit status 1. Output: invalid echo type: invalid
   175  FAIL
   176  exit status 1
   177  FAIL	github.com/nmiyake/echgo/integration_test	1.564s
   178  ```
   179  
   180  We can see that the test now fails as expected. Since this is the expected behavior, update the test to pass when this
   181  happens and run the test again:
   182  
   183  ```
   184  ➜ echo '// Copyright (c) 2017 Author Name
   185  //
   186  // Licensed under the Apache License, Version 2.0 (the "License");
   187  // you may not use this file except in compliance with the License.
   188  // You may obtain a copy of the License at
   189  //
   190  //     http://www.apache.org/licenses/LICENSE-2.0
   191  //
   192  // Unless required by applicable law or agreed to in writing, software
   193  // distributed under the License is distributed on an "AS IS" BASIS,
   194  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   195  // See the License for the specific language governing permissions and
   196  // limitations under the License.
   197  
   198  package integration_test
   199  
   200  import (
   201  	"os/exec"
   202  	"testing"
   203  
   204  	"github.com/palantir/godel/pkg/products"
   205  )
   206  
   207  func TestInvalidType(t *testing.T) {
   208  	echgoPath, err := products.Bin("echgo")
   209  	if err != nil {
   210  		panic(err)
   211  	}
   212  	cmd := exec.Command(echgoPath, "-type", "invalid", "foo")
   213  	output, err := cmd.CombinedOutput()
   214  	gotOutput := string(output)
   215  	if err == nil {
   216  		t.Errorf("expected command %v to fail. Output: %s", cmd.Args, gotOutput)
   217  	}
   218  	wantOutput := "invalid echo type: invalid\\n"
   219  	if wantOutput != gotOutput {
   220  		t.Errorf("invalid output: want %q, got %q", wantOutput, gotOutput)
   221  	}
   222  	wantErr := "exit status 1"
   223  	gotErr := err.Error()
   224  	if wantErr != gotErr {
   225  		t.Errorf("invalid error output: want %q, got %q", wantErr, gotErr)
   226  	}
   227  }' > integration_test/integration_test.go
   228  ➜ go test -v ./integration_test
   229  === RUN   TestInvalidType
   230  --- PASS: TestInvalidType (0.51s)
   231  PASS
   232  ok  	github.com/nmiyake/echgo/integration_test	0.707s
   233  ```
   234  
   235  We can see that the test now passes. The test will now run when `./godelw test` is invoked.
   236  
   237  One thing to note about this construction is that the `go build` and `go install` commands will currently not work for
   238  `integration_test` because the directory contains only tests:
   239  
   240  ```
   241  ➜ go build ./integration_test
   242  go build github.com/nmiyake/echgo/integration_test: no non-test Go files in /Volumes/git/go/src/github.com/nmiyake/echgo/integration_test
   243  ```
   244  
   245  We can work around this by adding a `doc.go` file to the directory to act as a placeholder:
   246  
   247  ```
   248  ➜ echo '// Copyright (c) 2017 Author Name
   249  //
   250  // Licensed under the Apache License, Version 2.0 (the "License");
   251  // you may not use this file except in compliance with the License.
   252  // You may obtain a copy of the License at
   253  //
   254  //     http://www.apache.org/licenses/LICENSE-2.0
   255  //
   256  // Unless required by applicable law or agreed to in writing, software
   257  // distributed under the License is distributed on an "AS IS" BASIS,
   258  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   259  // See the License for the specific language governing permissions and
   260  // limitations under the License.
   261  
   262  // Package integration contains integration tests.
   263  package integration' > integration_test/doc.go
   264  ```
   265  
   266  Verify that building the directory no longer fails:
   267  
   268  ```
   269  ➜ go build ./integration_test
   270  ```
   271  
   272  Run `./godelw test` to verify that this test is run:
   273  
   274  ```
   275  ➜ ./godelw test
   276  ok  	github.com/nmiyake/echgo                 	0.189s [no tests to run]
   277  ok  	github.com/nmiyake/echgo/echo            	0.201s
   278  ok  	github.com/nmiyake/echgo/generator       	0.179s [no tests to run]
   279  ok  	github.com/nmiyake/echgo/integration_test	0.614s
   280  ```
   281  
   282  The configuration in `godel/config/test.yml` can be used to group tests into tags. Update the configuration as follows:
   283  
   284  ```
   285  ➜ echo 'tags:
   286    integration:
   287      names:
   288        - "^integration_test$"' > godel/config/test.yml
   289  ```
   290  
   291  This configuration defines a tag named "integration" that matches any directories named "integration_test". Run the
   292  following command to run only the tests that match the "integration" tag:
   293  
   294  ```
   295  ➜ ./godelw test --tags=integration
   296  ok  	github.com/nmiyake/echgo/integration_test	0.583s
   297  ```
   298  
   299  By default, the `./godelw test` task runs all tests (all tagged and untagged tests). Multiple tags can be specified by
   300  separating them with a comma. Specifying `all` will run all tagged tests, while specifying `none` will run all tests
   301  that do not match any tags.
   302  
   303  Commit these changes by running the following:
   304  
   305  ```
   306  ➜ git add godel main.go integration_test vendor
   307  ➜ git commit -m "Add integration tests"
   308  [master 676aad3] Add integration tests
   309   6 files changed, 236 insertions(+), 1 deletion(-)
   310   create mode 100644 integration_test/doc.go
   311   create mode 100644 integration_test/integration_test.go
   312   create mode 100644 vendor/github.com/palantir/godel/pkg/products/products.go
   313   create mode 100644 vendor/github.com/palantir/godel/pkg/products/products_test.go
   314  ```
   315  
   316  Tutorial end state
   317  ------------------
   318  
   319  * `$GOPATH/src/github.com/nmiyake/echgo` exists and is the working directory
   320  * Project contains `godel` and `godelw`
   321  * Project contains `main.go`
   322  * Project contains `.gitignore` that ignores IDEA files
   323  * Project contains `echo/echo.go`, `echo/echo_test.go` and `echo/echoer.go`
   324  * `godel/config/dist.yml` is configured to build `echgo`
   325  * Project is tagged as 0.0.1
   326  * `godel/config/dist.yml` is configured to create distributions for `echgo`
   327  * Project is tagged as 0.0.2
   328  * Go files have license headers
   329  * `godel/config/generate.yml` is configured to generate string function
   330  * `godel/config/exclude.yml` is configured to ignore all `.+_string.go` files
   331  * `integration_test` contains integration tests
   332  * `godel/config/test.yml` is configured to specify the "integration" tag
   333  
   334  ([Link](https://github.com/nmiyake/echgo/tree/676aad36a5c355af826397be682f49bbb4a9ed20))
   335  
   336  Tutorial next step
   337  ------------------
   338  
   339  [Sync documentation with GitHub wiki](https://github.com/palantir/godel/wiki/GitHub-wiki)