GithubHelp home page GithubHelp logo

suite's Introduction

Suite

Standard Test Go Reference

Suite is a package meant to make testing gobuffalo.io applications easier.

Setup

This is the entry point into your unit testing suite. The Test_ActionSuite(t *testing.T) function is compatible with the go test command, and it should:

  • Create and configure your new test suite instance (ActionSuite in this case)
  • Call suite.Run with the *testing.T passed by the Go testing system, and your new ActionSuite instance
package actions_test

import (
    "testing"

    "github.com/gobuffalo/suite/v4"
    "github.com/gobuffalo/toodo/actions"
)

type ActionSuite struct {
    *suite.Action
}

func Test_ActionSuite(t *testing.T) {
    as := &ActionSuite{suite.NewAction(actions.App())}
    suite.Run(t, as)
}

Usage

This is where you write your actual test logic. The rules for test names are similar, but not the same, as with go test:

  • Each test is a method on your *ActionSuite
  • Test method names should start with Test (note the upper case T)
  • Test methods should have no arguments

A few additional notes:

  • To avoid race conditions on the testing database, always use the ActionSuite variable called DB to access the database (not your production app's database)
  • You can access the raw *testing.T value if needed with as.T()
  • ActionSuite has support for testify's require package and assert package
  • ... So try to use one of those instead packages of using the raw methods on the *testing.T
  • The default database that suite will connect to is called testing in your database.yml
package actions_test

import (
    "fmt"

    "github.com/gobuffalo/toodo/models"
)

func (as *ActionSuite) Test_TodosResource_List() {
    todos := models.Todos{
        {Title: "buy milk"},
        {Title: "read a good book"},
    }
    for _, t := range todos {
        err := as.DB.Create(&t)
        as.NoError(err)
    }

    res := as.HTML("/todos").Get()
    body := res.Body.String()
    for _, t := range todos {
        as.Contains(body, fmt.Sprintf("<h2>%s</h2>", t.Title))
    }
}

func (as *ActionSuite) Test_TodosResource_New() {
    res := as.HTML("/todos/new").Get()
    as.Contains(res.Body.String(), "<h1>New Todo</h1>")
}

func (as *ActionSuite) Test_TodosResource_Create() {
    todo := &models.Todo{Title: "Learn Go"}
    res := as.HTML("/todos").Post(todo)
    as.Equal(301, res.Code)
    as.Equal("/todos", res.Location())

    err := as.DB.First(todo)
    as.NoError(err)
    as.NotZero(todo.ID)
    as.NotZero(todo.CreatedAt)
    as.Equal("Learn Go", todo.Title)
}

func (as *ActionSuite) Test_TodosResource_Create_Errors() {
    todo := &models.Todo{}
    res := as.HTML("/todos").Post(todo)
    as.Equal(422, res.Code)
    as.Contains(res.Body.String(), "Title can not be blank.")

    c, err := as.DB.Count(todo)
    as.NoError(err)
    as.Equal(0, c)
}

func (as *ActionSuite) Test_TodosResource_Update() {
    todo := &models.Todo{Title: "Lern Go"}
    verrs, err := as.DB.ValidateAndCreate(todo)
    as.NoError(err)
    as.False(verrs.HasAny())

    res := as.HTML("/todos/%s", todo.ID).Put(&models.Todo{ID: todo.ID, Title: "Learn Go"})
    as.Equal(200, res.Code)

    err = as.DB.Reload(todo)
    as.NoError(err)
    as.Equal("Learn Go", todo.Title)
}

Fixtures (Test Data)

Often it is useful to load a series of data into the database at the start of the test to make testing easier. For example, you need to have a user in the database to log a person into the application, or you need some data in the database to test destroying that data. Fixtures let us solve these problems easily.

Using Fixtures

First you need to setup your test suite to use fixtures. You can do this by using suite.NewActionWithFixtures or suite.NewModelWithFixtures methods to create new test suites that take an fs.FS pointing to where the files for this suite live.

package actions

import (
    "os"
    "testing"

    "github.com/gobuffalo/suite/v4"
)

type ActionSuite struct {
    *suite.Action
}

func Test_ActionSuite(t *testing.T) {
    action, err := suite.NewActionWithFixtures(App(), os.DirFS("../fixtures"))
    if err != nil {
        t.Fatal(err)
    }

    as := &ActionSuite{
        Action: action,
    }
    suite.Run(t, as)
}

Once your suite is set up, you can create N numbers of *.toml files in the directory you've chosen for your fixtures, in this example, ../fixtures.

Example Fixture File

[[scenario]]
name = "lots of widgets"

  [[scenario.table]]
    name = "widgets"

    [[scenario.table.row]]
      id = "<%= uuidNamed("widget") %>"
      name = "This is widget #1"
      body = "some widget body"
      created_at = "<%= now() %>"
      updated_at = "<%= now() %>"

    [[scenario.table.row]]
      id = "<%= uuid() %>"
      name = "This is widget #2"
      body = "some widget body"
      created_at = "<%= now() %>"
      updated_at = "<%= now() %>"

  [[scenario.table]]
    name = "users"

    [[scenario.table.row]]
      id = "<%= uuid() %>"
      name = "Mark Bates"
      admin = true
      price = 19.99
      widget_id = "<%= uuidNamed("widget") %>"
      created_at = "<%= now() %>"
      updated_at = "<%= now() %>"

Helper Methods

The *.toml files all get run through https://github.com/gobuffalo/plush before they're decoded, so you can make use of the helpful helper methods that ship with Plush.

We've also add a couple of useful helpers for you as well:

  • uuid() - returns a new github.com/gobuffalo/uuid.UUID
  • now() - returns time.Now()
  • nowAdd(s) and nowSub(s) - similar to now() but s amount of seconds is added or substracted, respectively, from the return value
  • uuidNamed(name) - will attempt to return a previously declared UUID with that name, useful, for relations/associations. If there was one that wasn't defined with that name, a new one will be created.
  • hash(string, opts) - will create the hashed value of the string (useful for creating a password), you can define the cost as an opts (the default is bcrypt.DefaultCost)

Using in Tests

In your suite tests you need to call the LoadFixture method giving it the name of the fixtures you would like to use for this test.

func (as *ActionSuite) Test_WidgetsResource_List() {
    as.LoadFixture("lots of widgets")
    res := as.HTML("/widgets").Get()

    body := res.Body.String()
    as.Contains(body, "widget #1")
    as.Contains(body, "widget #2")
}

FAQs

  • Can I call LoadFixture more than once in a test? - Absolutely! Call it as many times as you want!
  • Can I load multiple rows into a table in one scenario? - Absolutely!
  • Can I load data into multiple tables in one scenario? - Absolutely!
  • Will it load all my fixtures? - No, you have to load specific scenarios, so don't be afraid to create lots of scenarios and only call the ones you need per test.
  • Will this pollute my database, and how do I clear data between tests? - No need to worry, the suite will truncate any data in your database between test runs, so you never have to worry about it.

suite's People

Contributors

adamlamar avatar arschles avatar breml avatar dependabot[bot] avatar fasmat avatar kteb avatar linus-boehm avatar lukasschlueter avatar markbates avatar paganotoni avatar robbyoconnor avatar robsliwi avatar sio4 avatar slomek avatar stanislas-m avatar tsmith1024 avatar ypjama avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

suite's Issues

Error in sub test raises leads to panic: subtest may have called FailNow on a parent test

When using sub tests with suite.Action or suite.Model and an assertion fails in the sub test, the following error messing is shown:

testing.go:1033: test executed panic(nil) or runtime.Goexit: subtest may have called FailNow on a parent test

The reason for this issue is, that m.Assertions is not updated with the correct *testing.T, when the sub test is started with Run(...) and therefore still the *testing.T of the parent test is used.

always return ok

only run suite.Run action ,others not work. so always return ok;
image

Is there any reason `suite` does not integrated with `httptest` instead of `willie`?

Hi @markbates,
Is there any reason Suite does not yet integrated with httptest instead of willie? As I understand httptest obsolete willie and when I run buffalo fix, it replaces all imports of willie with httptest but currently it seems not working properly without willie since it still depends on willie.

With below patch and small modification on my test cases, it works.

--- a/suite.go
+++ b/suite.go
@@ -4,9 +4,9 @@ import (
        "testing"
 
        "github.com/gobuffalo/buffalo"
+       "github.com/gobuffalo/httptest"
        "github.com/gobuffalo/mw-csrf"
        "github.com/gobuffalo/packr"
-       "github.com/markbates/willie"
        "github.com/pkg/errors"
        "github.com/stretchr/testify/suite"
 )
@@ -14,7 +14,6 @@ import (
 type Action struct {
        *Model
        Session *buffalo.Session
-       Willie  *willie.Willie
        App     *buffalo.App
        csrf    buffalo.MiddlewareFunc
 }
@@ -43,12 +42,12 @@ func Run(t *testing.T, s suite.TestingSuite) {
        suite.Run(t, s)
 }
 
-func (as *Action) HTML(u string, args ...interface{}) *willie.Request {
-       return as.Willie.HTML(u, args...)
+func (as *Action) HTML(u string, args ...interface{}) *httptest.Request {
+       return httptest.New(as.App).HTML(u, args...)
 }
 
-func (as *Action) JSON(u string, args ...interface{}) *willie.JSON {
-       return as.Willie.JSON(u, args...)
+func (as *Action) JSON(u string, args ...interface{}) *httptest.JSON {
+       return httptest.New(as.App).JSON(u, args...)
 }
 
 func (as *Action) SetupTest() {
@@ -65,7 +64,6 @@ func (as *Action) SetupTest() {
                        return next(c)
                }
        }
-       as.Willie = willie.New(as.App)
 }
 
 func (as *Action) TearDownTest() {

Since handler of httptest was not exported, I just use above approach. Is there any reason the handler was not exported? anyway it looks fine.

Cannot use pop v5 with suite.

It seems Suite depends on a version of 0-version of pop. That causes that we cannot use pop v5 with it, I think we should make that pop.Connection an interface in here or launch a new version that depends on pop v5.

@gobuffalo/all, thoughts?

Is it possible to modify action's REMOTE_ADDR

Hi,
I am writing an unit test for an API Handler, which is applied a UserIPMiddleware
This middleware is to restrict the client IP access

func UserIPMiddleware(next buffalo.Handler) buffalo.Handler {
	return func(c buffalo.Context) error {
		var userIp string
		if h, _, err := net.SplitHostPort(c.Request().RemoteAddr); err == nil {
			userIp = h
		} else {
			return c.Error(http.StatusUnauthorized, fmt.Errorf("cannot get client IP"))
		}

		if !isInternalAcceptIp(userIp) {
			c.Logger().Warnf("Invalid IP access: %v, request path: %v", userIp, c.Request().URL)
			return c.Error(http.StatusUnauthorized, fmt.Errorf("invalid IP access: %v, request path: %v", userIp, c.Request().URL))
		}

		c.Set("user_ip", userIp)
		c.Logger().Debugf("Acceptable internal IP is %v", userIp)
		return next(c)
	}
}

Here is the unit test, such as

func (as *ActionSuite) Test_createUser_via_externalIP() {
	req := createUserRequest{
		Name: "tester"
	}
	
	req := as.JSON("/api/users")
	res := req.Post(req)

	as.Equal(http.StatusUnauthorized, res.Code)
}

So my question is, can I modify the action's REMOTE_ADDR to fake it like an external IP?

Modifying `now()`

Is there a clean way to alter the now() return value to be in the future?

[[scenario.table.row]]
  name = "15% Cashback"
  description = "Get 15% cashback when you purchase a cup of coffee from our participating outlets"
  starts_at = "<%= now() %>"
  ends_at = "<%= now() %>" <-- modify this to be later
  created_at = "<%= now() %>"
  updated_at = "<%= now() %>"

[Model]: Deadlock using postgres

When using a ModelSuite like in the documentation and create multiple tests using fixtures with a postgres database, I get a deadlock leading to a 10 minutes timeout.

Looking into the postgres database, I was able to find a lot of statements created by the postgresql.TruncateAll method.

I followed the problem a little further:

  • As the Tests are initialized in parallel, the SetupTest methods of all tests are executed in parallel
  • So, for every Test, the TruncateAll is executed at once
  • As the command created by the postgres dialect module executed a SQL TRUNCATE command for every table in the DB
  • TRUNCATE commands in psql create exclusive locks, which leads to a deadlock.

I found a solution suggesting using DELETE instead of TRUNCATE as a possible solution for this, which might resolve the deadlock, but still will lead to unexpected behaviour due to the parallel running SetupTest executions.

Any Idea how to solve this? Any suggestions for workarounds?

Additionally: I cannot find any model unit test examples. Are there any available somewhere or has this not been considered yet?

Thank you in advance.

Post doesn't work as expected

Hi,
In my test I am doing this:

newUser := &models.User{
		Name: "New user",
		Email: "[email protected]",
	}
res := as.HTML("/users").Post(newUser)

The code seems to be identical as in the example.
However, I get an error: invalid character 'C' looking for beginning of value

When I log c.Request().Body in my action I get
CreatedAt=&Email=new%40test.com&ID=00000000-0000-0000-0000-000000000000&Name=New+user&UpdatedAt=
I understand that is where 'C' in the error comes from.

However, if I test my endpoint from Postman in the live app, c.Request().Body logs

{
	"name": "New User",
	"email": "[email protected]"
}

It seams like Post method sends my model's data as parameters instead of json body.
How could this be fixed?

Thank you for your help!

What happened with v2.1.5?

Our Athens builds started failing today with a Hash Mismatch:

go: verifying github.com/gobuffalo/[email protected]+incompatible: checksum mismatch
    downloaded: h1:bNsqms032vutuSyznscA5s2pwv/RdsDYhE3m4ChOLXA=
    go.sum:     h1:u3fPfi47UUm656kCvXNm4R9L0Rn8tN0SYFW4F51lVhE=
buffalo: 2018/09/06 17:02:20 === Error! ===
buffalo: 2018/09/06 17:02:20 === exit status 1

(For full build details: https://travis-ci.org/gomods/athens/builds/425360844?utm_source=github_status&utm_medium=notification)

Looking at the releases (https://github.com/gobuffalo/suite/releases): they show that the v2.1.5 was updated 2018-09-05 (see image below) even though the tag was actually published 7 days before that (see our commit when we included v2.1.5).

When v2.1.5 tag was "updated"
screen shot 2018-09-06 at 4 31 04 pm

When tag was added to Athens:
gomods/athens@e2cd124#diff-37aff102a57d3d7b797f152915a6dc16R19

Ignoring TruncateAll() error breaks tests

In commit 25ca460, the error to as.DB.TruncateAll() was ignored. This causes silent failure of the truncate step.

In my particular case, I found that the database was truncated before tests on sqlite but not postgres, which led to broken tests (due to a uniqueness constraint).

After restoring the error check, I received this error:

pq: could not access file "$libdir/plpgsql": No such file or directory

which apparently is caused by installing multiple versions of postgres via homebrew on mac.

Would be great to restore the error check to make it easier to identify this issue.

How to use scenarios?

I see the .Find("lots of widgets") in the source for when you have multiple scenarios but I'm unsure of how to map use this inside of a test. How are scenarios found or called once they are defined inside of a fixture?

Package requires github.com/gofrs/uuid/v3, however the rest of buffalo uses github.com/gofrs/uuid.

The version of uuid in this package does not line up with the other buffalo packages, which all still use github.com/gofrs/uuid.

For example, Pop v5: https://github.com/gobuffalo/pop/blob/master/go.mod, uses github.com/gofrs/uuid fixed to v3.2.

This can cause issues, due to types mismatching.

In my case, I will load a fixture in my test, get the value of a UUID using a plush.Context, and then try to supply that to a struct of mine. In order to do so, I need to add an additional conversion from github.com/gofrs/uuid/v3.UUID to github.com/gofrs/uuid.UUID.

[Feature Request] LoadFixture to return the Scenario object

Currently LoadFixture is a void method; I think it would be useful to return the Scenario object used to create the fixture. Rails has the same feature, where all fixtures used in the test is accessible via @variablename.

This is beneficial for the following reasons:

  • One source of truth for what the data should be
    • Prevents typo on assertions
    • Changing hard-coded values on fixtures do not require change in the test
    • Developers do not need to remember hard-coded values they put in the fixtures
  • Avoids users trying to parse toml files on their own

In terms of backwards compatibility, this change will not affect current implementation since currently LoadFixture is a void method so the Scenario object returned will just not be used by existing code.

I can help make a PR for this change, but want to confirm with the community that this is something that we want before I start working on it.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.