Comments (6)
You're very welcome. Thank you for the compliment. I'm glad to hear you're finding it useful.
Great question (and I agree, having something explicit about this in the README is a good idea).
What I'd recommend is similar to what was done for the database. Add this new service to the container as an interface and when initializing it in NewContainer()
check if the environment is config.EnvTest
, and if so, use a mock so you avoid making actual REST calls. Here is an example of that check: https://github.com/mikestefanello/pagoda/blob/main/pkg/services/container.go#L149. You just need to make sure you set the environment to test (ie, https://github.com/mikestefanello/pagoda/blob/main/pkg/services/services_test.go#L20-L25) prior to creating the container. For the DB, it connects to a test database rather than the primary database for tests.
To the best of my knowledge, there isn't an official way to determine if you're running within a test, so manually setting this environment is still required.
Let me know if that works for you.
from pagoda.
That worked!
However, I slightly dislike the following:
- Mixing test-related code with production code, to define and create the mock struct and behavior.
- Technically, the code path executed during tests is different from that executed in normal execution. I understand that this is ultimately unavoidable when using mocks, but having both code paths in the production code is different form the test clearly overriding/mocking specific functions as a part of the test setup code.
I was thinking about perhaps extending the DI approach to creating the service container itself, to enable swapping its own dependencies: the service clients. For example, allowing services.NewContainer()
to optionally accept parameters that would override the corresponding services, such as services.NewContainer(db)
to pass the test db connection (or a struct/interface that can create it). This way, test setup, such as services.TestMain
(in services_test.go
) can override whichever container dependencies to use test/mock ones.
Any thoughts on the current opaque container vs one that acknowledges its dependencies (or at least some of them) and allows them to be (optionally) passed as parameters?
Any advantages of the current approach over this?
from pagoda.
You bring up good points. Similar to what you suggested, what do you think about either of these?
- Create
NewTestContainer()
which is more explicit than checking the environment, and initializes everything in test/mock mode. - In your tests (ie, in
TestMain()
) after you doc := NewContainer()
, you can swap in mocks, likec.MyService = NewMockService()
. I'm not a fan of this one because it's potentially duplicated, and you need to make sure you always remember to swap what you intend to. - If the code you're testing using one or a very small amount of services on the container, you can just create a container in that test or test package with only the services you need, mocked however you want, etc.
from pagoda.
I tried option 1, and it seems to work. Here is what I tried:
-
Remove all
EnvTest
related conditions from theservices
package and kept only production services. For example,initDatabase()
incontainer.go
now looks like:func (c *Container) getDatabaseAddr(dbName string) string { return fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", c.Config.Database.User, c.Config.Database.Password, c.Config.Database.Hostname, c.Config.Database.Port, dbName, ) } // initDatabase initializes the database func (c *Container) initDatabase() { var err error c.Database, err = sql.Open("pgx", c.getDatabaseAddr(c.Config.Database.Database)) if err != nil { panic(fmt.Sprintf("failed to connect to database: %v", err)) } }
I had to move
getAddr()
outside ofinitDatabase()
so I can use it ininitTestDatabase()
(see below). -
Implement
NewTestContainer()
and its test dependencies inservices_test.go
(in my case, I need a mock Auth service):func NewTestContainer() *Container { c := new(Container) c.initConfig() c.initValidator() c.initWeb() c.initTestCache() // Test Cache c.initTestDatabase() // Test Database c.initORM() c.initTags() c.initChannels() c.initUsers() c.initMockAuth() // Mock Auth c.initTemplateRenderer() c.initMail() c.initTasks() return c } func (c *Container) initTestCache() { var err error if c.Cache, err = NewTestCacheClient(c.Config); err != nil { panic(err) } } func (c *Container) initTestDatabase() { var err error // Connect to the test database server c.Database, err = sql.Open("pgx", c.getDatabaseAddr("postgres")) if err != nil { panic(fmt.Sprintf("failed to connect to database: %v", err)) } // Drop the test database, ignoring errors in case it doesn't yet exist _, _ = c.Database.Exec("DROP DATABASE " + c.Config.Database.TestDatabase) // Create the test database if _, err := c.Database.Exec("CREATE DATABASE " + c.Config.Database.TestDatabase); err != nil { panic(fmt.Sprintf("failed to create test database: %v", err)) } c.Database, err = sql.Open("pgx", c.getDatabaseAddr(c.Config.Database.TestDatabase)) if err != nil { panic(fmt.Sprintf("failed to connect to database: %v", err)) } } func (c *Container) initMockAuth() { c.Auth = newMockAuthClient(c.Users) }
-
Implement
NewTest[Service]Client()
(orNewMock[Service]Client()
) in the service's respective[service]_test.go
. For example, incache_test.go
:func NewTestCacheClient(cfg *config.Config) (*CacheClient, error) { db := cfg.Cache.TestDatabase // Connect to the cache c := &CacheClient{} c.Client = redis.NewClient(&redis.Options{ Addr: fmt.Sprintf("%s:%d", cfg.Cache.Hostname, cfg.Cache.Port), Password: cfg.Cache.Password, DB: db, }) if _, err := c.Client.Ping(context.Background()).Result(); err != nil { return c, err } // Flush the database in the test environment if err := c.Client.FlushDB(context.Background()).Err(); err != nil { return c, err } cacheStore := redisstore.NewRedis(c.Client) c.cache = cache.New[any](cacheStore) return c, nil }
Comments:
- I like that all test code is exclusively in
*_test.go
files, and none of it leaks into production code. - I'm not sure about mixing code for test cases with code for test setup, especially in
[service]_test.go
files as shown incache_test.go
. If there is a way to separate them, that'd be great. - I initially tried to keep
NewTestContainer()
and related test setup code (e.g.initTestDatabase()
) in thetests
package (pkg/tests
), but ran into dependency cycles. - Any comments about the locations of each piece of the test code? Any suggested improvements?
- I was expecting DI to offer a cleaner way of injecting mock/test implementations. I have to say, I'm not very experienced with DI, but this is one of the most cited advantages of DI, but I can't see it.
About option 2: I share your dislike to option 2, since it unnecessarily creates the non-test clients then overwrites them.
About option 3: I think option 3 is a variation of option 1, in which NewTestContainer()
creates a subset of the services. The different containers for different tests would require somewhat redundant code. Instead of duplicating the NewTestContainer()
code with different created services, I'd rather have that code once and reuse it.
from pagoda.
Keeping NewTestContainer()
in services_test.go
is a no-go :/
It cannot be imported in other packages tests, e.g. routes/routes_test.go
.
from pagoda.
Related Issues (20)
- Gorilla no longer maintained HOT 3
- Rebuilding after file edits HOT 1
- How to use pongo2 as default template engine. HOT 11
- Files HOT 5
- Rendering data returned by ent query HOT 1
- sqlc instead of ent HOT 1
- API support HOT 7
- Workflow on making migrations with Ent and Atlas? HOT 3
- Syntax Highlighting for gohtml files HOT 1
- Error initializing ORM HOT 3
- Flash messages when using HTMX HOT 2
- Can't update Go and deps cleanly HOT 5
- htmx problem HOT 1
- Feature idea: Deployment examples HOT 10
- Switching to embed.FS broke hot-reload of templates HOT 1
- A few suggestions / questions from a first time user HOT 10
- Problem implementing redirect with querystring parameters HOT 2
- Request for Tailwind CSS Support HOT 3
- Create a space for discussions, support and future roadmap planning HOT 2
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from pagoda.