{"id":15170772,"url":"https://github.com/sombriks/sample-testcontainers","last_synced_at":"2026-01-25T03:01:33.054Z","repository":{"id":244720880,"uuid":"814328807","full_name":"sombriks/sample-testcontainers","owner":"sombriks","description":"samples on why and how to use testcontainers","archived":false,"fork":false,"pushed_at":"2025-01-14T18:49:47.000Z","size":2806,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-14T15:18:56.218Z","etag":null,"topics":["alpinejs","bulma","echo","gomponents","goqu","htmx","ionicons","jpa","knex","koa","postgresql","pug","spring-boot","testcontainers","testcontainers-go","testcontainers-kotlin","testcontainers-node","thymeleaf"],"latest_commit_sha":null,"homepage":"https://sombriks.com/blog/0071-test-containers-in-3-languages/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sombriks.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-06-12T19:46:45.000Z","updated_at":"2025-01-14T18:49:52.000Z","dependencies_parsed_at":"2024-06-17T01:33:23.895Z","dependency_job_id":"d1f494f1-0bb9-4122-b538-3374049c2a19","html_url":"https://github.com/sombriks/sample-testcontainers","commit_stats":{"total_commits":50,"total_committers":1,"mean_commits":50.0,"dds":0.0,"last_synced_commit":"279ceb8e2def0b1cac39e2596b95642092703d10"},"previous_names":["sombriks/sample-testcontainers"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/sombriks/sample-testcontainers","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sombriks%2Fsample-testcontainers","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sombriks%2Fsample-testcontainers/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sombriks%2Fsample-testcontainers/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sombriks%2Fsample-testcontainers/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sombriks","download_url":"https://codeload.github.com/sombriks/sample-testcontainers/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sombriks%2Fsample-testcontainers/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28742973,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-25T02:46:29.005Z","status":"ssl_error","status_checked_at":"2026-01-25T02:44:29.968Z","response_time":113,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alpinejs","bulma","echo","gomponents","goqu","htmx","ionicons","jpa","knex","koa","postgresql","pug","spring-boot","testcontainers","testcontainers-go","testcontainers-kotlin","testcontainers-node","thymeleaf"],"created_at":"2024-09-27T08:22:52.330Z","updated_at":"2026-01-25T03:01:33.040Z","avatar_url":"https://github.com/sombriks.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# [Sample Testcontainers][repo]\n\nSamples on why and how to use [TestContainers][testcontainers]\n\n[![GO CI](https://github.com/sombriks/sample-testcontainers/actions/workflows/go.yml/badge.svg)](https://github.com/sombriks/sample-testcontainers/actions/workflows/go.yml)\n[![JVM CI](https://github.com/sombriks/sample-testcontainers/actions/workflows/jvm.yml/badge.svg)](https://github.com/sombriks/sample-testcontainers/actions/workflows/jvm.yml)\n[![Node CI](https://github.com/sombriks/sample-testcontainers/actions/workflows/node.yml/badge.svg)](https://github.com/sombriks/sample-testcontainers/actions/workflows/node.yml)\n\n\u003cvideo width=\"320\" height=\"240\" controls\u003e\n  \u003csource src=\"./docs/sample-kanban-2024-06-25-11-16-07.mp4\" type=\"video/mp4\"\u003e\n\u003c/video\u003e\n\n## Test Boundaries\n\nUntested code is a dark jungle filled with unknown bugs. \n\nWe write tests to light up a fire to keep unexpected problems away.\n\nBut how far should a test suite should go?\n\nIt's clear that any business-specific code must be covered with tests, but does\na 3rd party API endpoint should be tested too? And the database?\n\nThere are frontiers. Anything out of our control can not be properly tested.\n\nAnd this is the crossroads: expand our control or mock boundaries.\n\n## The problem with too much mocks\n\nDon't get me wrong, mocks at the boundaries works. But as advised by Mockito\nfront page project, _don't mock everything_.\n\nFor example, this mock looks perfectly reasonable:\n\n```kotlin\n// mock to list data - ok\n@BeforeEach\nfun setup() {\n    _when(\n        personRepository.findByNameContainingIgnoreCase(\n            anyString(), anyOrNull()\n        )\n    ).thenReturn(personPage)\n}\n\n@Test\nfun `should list people`() {\n    val result = boardService.listPeople(\"\", pageable)\n    assertThat(result, notNullValue())\n}\n```\n\nBut then:\n\n```kotlin\n// mock to insert - fail\n@Test\n@Disabled(\"We can keep mocking but we don't trust the test anymore\")\nfun `should save people`() {\n    val person = Person(name = \"Ferdinando\")\n    boardService.savePerson(person)\n    assertThat(person.id, notNullValue()) //new person should have an id now\n}\n```\n\nIn this situation you can simply keep growing the mock surface but there will be\na point when you will be testing nothing at all.\n\nTo really solve it, your boundaries must expand. And if the boundary to expand\nis the database, here goes some samples.\n\n## Introducing TestContainers\n\nOne way to test the database is to use some lightweight database runtime like h2\nor sqlite, but that comes with a price: the dialect might be different from the\nreal deal and therefore you must be cautious about your queries.\n\nTo properly avoid that, it's ideal to use same RDBMS for development, staging\nand for testing.\n\nUsing TestContainers makes this task a real easy breeze.\n\n## Testing the database\n\nWhenever we need to \"test the database\", what we're really testing is a known\ndatabase state. We expect a certain user/password to be accepted; we expect a\ncertain schema and a set of tables to exists. We expect some data to be present.\n\nTherefore, when spinning up a test suite involving relational data, some setup\nis needed. And TestContainers offers goodies to be used exactly in that phase.\n\n### Sample code - Spring/Kotlin/JUnit\n\nSpring tests has not only the setup phase but also The @TestConfiguration\nstereotype, so the DI container does all the heavy-lifting for you:\n\n```kotlin\npackage sample.testcontainer.kanban\n\nimport org.springframework.beans.factory.annotation.Value\nimport org.springframework.boot.test.context.TestConfiguration\nimport org.springframework.boot.testcontainers.service.connection.ServiceConnection\nimport org.springframework.context.annotation.Bean\nimport org.testcontainers.containers.PostgreSQLContainer\nimport org.testcontainers.utility.DockerImageName\n\n@TestConfiguration(proxyBeanMethods = false)\nclass TestcontainersConfiguration {\n\n    @Value(\"\\${database}\")\n    private lateinit var database: String\n    @Value(\"\\${spring.datasource.username}\")\n    private lateinit var username: String\n    @Value(\"\\${spring.datasource.password}\")\n    private lateinit var password: String\n\n    @Bean\n    @ServiceConnection\n    fun postgresContainer(): PostgreSQLContainer\u003c*\u003e {\n        return PostgreSQLContainer(\n            DockerImageName\n                .parse(\"postgres:16.3-alpine3.20\")\n        ).withEnv(\n            mapOf(\n                \"POSTGRES_DB\" to database,\n                \"POSTGRES_USER\" to username,\n                \"POSTGRES_PASSWORD\" to password\n            )\n        ).withInitScript(\"./initial-state.sql\")\n    }\n\n}\n\n```\n\nThis configuration should be \"imported\" into the test case so the default\ndatabase configuration, which probably won't be present in a CI workflow, can be\nreplaced in a transparent way. Someone at TestContainers team indeed made a fine\nwork on this craft:\n\n```kotlin\npackage sample.testcontainer.kanban\n\nimport org.junit.jupiter.api.Test\nimport org.springframework.boot.test.context.SpringBootTest\nimport org.springframework.context.annotation.Import\n\n@SpringBootTest\n// just add that and you have a full-featured, predictable, database for test!\n@Import(TestcontainersConfiguration::class)\nclass SampleKanbanJvmApplicationTests {\n\n\t@Test\n\tfun contextLoads() {\n\t}\n\n}\n```\n\n### Sample code - Koa/Knex/Ava\n\nAva has hooks where you can properly set up and tear down the database. Update\n[database configuration][node-tc] accordingly:\n\n```javascript\n// in app/configs/hook-test-container.js\nimport {resolve} from 'node:path';\nimport {PostgreSqlContainer} from '@testcontainers/postgresql';\n\n/**\n * Helper to provision a postgresql for testing purposes\n *\n * @returns {Promise\u003cStartedPostgreSqlContainer\u003e} database container\n */\nexport const preparePostgres = async () =\u003e new PostgreSqlContainer('postgres:16.3-alpine3.20')\n  .withDatabase(process.env.PG_DATABASE)\n  .withUsername(process.env.PG_USERNAME)\n  .withPassword(process.env.PG_PASSWORD)\n  .withBindMounts([{\n    source: resolve(process.env.PG_INIT_SCRIPT),\n    target: '/docker-entrypoint-initdb.d/init.sql',\n  }])\n  .start();\n```\n\nA quick note, but the node postgresql container has a distinct idiom for the\ninitial script when compared with jvm or golang versions. Those have a\n`withInitScript` builder call, while node version offer a more generic\n`withBindMounts` call.\n\nYou then integrate the test container provisioning into your ava test like this:\n\n```javascript\n// in app/app.spec.js\nimport request from 'supertest';\nimport test from 'ava';\nimport {prepareApp} from './main.js';\nimport {prepareDatabase} from './configs/database.js';\nimport {boardServices} from './services/board-services.js';\nimport {boardRoutes} from './routes/board-routes.js';\nimport {preparePostgres} from './configs/hook-test-container.js';\n\ntest.before(async t =\u003e {\n\t// TestContainer setup\n\tt.context.postgres = await preparePostgres();\n\n\t// Application setup properly tailored for tests\n\tconst database = prepareDatabase(t.context.postgres.getConnectionUri());\n\tconst service = boardServices({db: database});\n\tconst controller = boardRoutes({service});\n\n\tconst {app} = prepareApp({db: database, service, controller});\n\n\t// Context registering for proper teardown\n\tt.context.db = database;\n\tt.context.app = app;\n});\n\ntest.after.always(async t =\u003e {\n\tawait t.context.db.destroy();\n\tawait t.context.postgres.stop({timeout: 500});\n});\n\ntest('app should be ok', async t =\u003e {\n\tconst result = await request(t.context.app.callback()).get('/');\n\tt.is(result.status, 302);\n\tt.is(result.headers.location, '/board');\n});\n\ntest('db should be ok', async t =\u003e {\n\tconst {rows: [{result}]} = await t.context.db.raw('SELECT 1 + 1 as result');\n\tt.truthy(result);\n\tt.is(result, 2);\n});\n\ntest('should serve login and have users', async t =\u003e {\n\tconst result = await request(t.context.app.callback()).get('/login');\n\tt.is(result.status, 200);\n\tt.regex(result.text, /Alice|Bob|Caesar|Davide|Edward/);\n});\n```\n\nMind to write proper testable code: it's very tempting to just create and export\nyour objects directly from modules:\n\n```javascript\n// in app/configs/views.js\nimport {resolve} from 'node:path';\nimport Pug from 'koa-pug';\n\nexport const pug = new Pug({\n  viewPath: resolve('./app/templates'),\n});\n```\n\nIt's pretty fine most of the time, templates directory isn't likely to become a\nconfigurable thing, so it's ok.\n\nBut for proper testing you must provide inversion of control, dependency\ninversion, the **D** in *[SOLID][solid]*:\n\n```javascript\n// in app/configs/database.js\nimport Knex from 'knex';\n\nexport const prepareDatabase = (connection = process.env.PG_CONNECTION_URL) =\u003e Knex({\n  client: 'pg',\n  connection,\n});\n```\n\nThe `prepareDatabase` call let us send any connection string we want for the\ndatabase, quite useful when we are spinning up a postgres container, but if none\nis provided it will rely on what we have configured in the environment under the\n`PG_CONNECTION_URL` variable.\n\nBesides that implementation detail, everything else should work under test the\nsame way it works during development or in production. same code, no mocks, same\ndatabase engine, same dialect, same thing.\n\n### Sample code - Echo/Goqu/Testify\n\n[Testify][testify] offers setup hooks where you can provision and later release the\ndatabase runtime.\n\n```go\npackage services\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/doug-martin/goqu/v9\"\n\t\"github.com/joho/godotenv\"\n\t\"github.com/sombriks/sample-testcontainers/sample-kanban-go/app/configs\"\n\t\"github.com/stretchr/testify/suite\"\n\t\"github.com/testcontainers/testcontainers-go\"\n\t\"github.com/testcontainers/testcontainers-go/modules/postgres\"\n\t\"github.com/testcontainers/testcontainers-go/wait\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype ServiceTestSuit struct {\n\tsuite.Suite\n\tctx     context.Context\n\ttc      *postgres.PostgresContainer\n\tdb      *goqu.Database\n\tservice *BoardService\n}\n\n// TestRunSuite when writing suites this is needed as a 'suite entrypoint'\n// see https://pkg.go.dev/github.com/stretchr/testify/suite\nfunc TestRunSuite(t *testing.T) {\n\tsuite.Run(t, new(ServiceTestSuit))\n}\n\nfunc (s *ServiceTestSuit) SetupSuite() {\n\tvar err error\n\t// Test execution point is inside the package, not in project root\n\t_ = godotenv.Load(\"../../.env\")\n\n\ts.ctx = context.Background()\n\n\tprops, err := configs.NewDbProps()\n\tif err != nil {\n\t\ts.Fail(\"Suite setup failed\", err)\n\t}\n\ts.tc, err = postgres.RunContainer(s.ctx,\n\t\ttestcontainers.WithImage(\"postgres:16.3-alpine3.20\"),\n\t\tpostgres.WithInitScripts(fmt.Sprint(\"../../\", props.InitScript)), // path changes due test entrypoint\n\t\tpostgres.WithUsername(props.Username),\n\t\tpostgres.WithDatabase(props.Database),\n\t\tpostgres.WithPassword(props.Password),\n\t\ttestcontainers.WithWaitStrategy(wait.\n\t\t\tForLog(\"database system is ready to accept connections\").\n\t\t\tWithOccurrence(2).\n\t\t\tWithStartupTimeout(10*time.Second)))\n\tif err != nil {\n\t\ts.Fail(\"Suite setup failed\", err)\n\t}\n\n\tdsn, err := s.tc.ConnectionString(s.ctx, fmt.Sprint(\"sslmode=\", props.SslMode))\n\tif err != nil {\n\t\ts.Fail(\"Suite setup failed\", err)\n\t}\n\n\ts.db, err = configs.NewGoquDb(nil, \u0026dsn)\n\tif err != nil {\n\t\ts.Fail(\"Suite setup failed\", err)\n\t}\n\n\ts.service, err = NewBoardService(s.db)\n\tif err != nil {\n\t\ts.Fail(\"Suite setup failed\", err)\n\t}\n\n}\n\nfunc (s *ServiceTestSuit) TearDownSuite() {\n\terr := s.tc.Terminate(s.ctx)\n\tif err != nil {\n\t\ts.Fail(\"Suite tear down failed\", err)\n\t}\n}\n\n// the test cases\n```\n\nSimilar to the advice givn on node version, mind the configuration phase! your\ncode is supposed to offer reasonable defaults and proper dependency injection so\nyou can provide test values or production values whenever needed:\n\n```go\npackage configs\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"github.com/doug-martin/goqu/v9\"\n\t_ \"github.com/doug-martin/goqu/v9/dialect/postgres\"\n\t_ \"github.com/lib/pq\"\n\t\"log\"\n)\n\n// NewGoquDb - provision a query builder instance\nfunc NewGoquDb(d *DbProps, dsn *string) (*goqu.Database, error) {\n\tvar err error\n\tif d == nil {\n\t\tlog.Println(\"[WARN] db props missing, creating a default one...\")\n\t\td, err = NewDbProps()\n\t}\n\n\t// configure the query builder\n\tif dsn == nil {\n\t\tnewDsn := fmt.Sprintf(\"postgresql://%s:%s@%s:5432/%s?sslmode=%s\", //\n\t\t\td.Username, d.Password, d.Hostname, d.Database, d.SslMode)\n\t\tdsn = \u0026newDsn\n\t} else {\n\t\tlog.Printf(\"[INFO] using provided dsn [%s]\\n\", *dsn)\n\t}\n\tcon, err := sql.Open(\"postgres\", *dsn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// https://doug-martin.github.io/goqu/docs/selecting.html#scan-struct\n\tgoqu.SetIgnoreUntaggedFields(true)\n\tdb := goqu.New(\"postgres\", con)\n\tdb.Logger(log.Default())\n\n\treturn db, nil\n}\n```\n\nThe sample above is called during configuration phase to provision the query\nbuilder instance; it receives, however, optional parameters that allow us to set\nappropriate values for development, test or production.\n\n## CI/CD integration\n\nNow the best part: most CI/CD infrastructure available out there will offer\ndocker runtimes, so your tests will run smoothly.\n\n_some sample code_\n\n## Conclusion\n\nNow that your boundaries got extended, your confidence on the code grows more\nand more. It does what it's supposed to do. It saves and list the expected\ncontent. It works*. As far as the tests can tell.\n\nThe complete source code can be found here.\n\nHappy hacking!\n\n[repo]: https://github.com/sombriks/sample-testcontainers\n[testcontainers]: https://testcontainers.com/\n[node-tc]: https://testcontainers.com/guides/getting-started-with-testcontainers-for-nodejs/\n[solid]: https://en.wikipedia.org/wiki/Dependency_inversion_principle\n[testify]: https://github.com/stretchr/testify\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsombriks%2Fsample-testcontainers","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsombriks%2Fsample-testcontainers","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsombriks%2Fsample-testcontainers/lists"}