├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── config.go ├── config_test.go ├── go.mod ├── go.sum ├── pgtest.go ├── pgtest_test.go └── test ├── Dockerfile-alpine ├── Dockerfile-fedora └── Dockerfile-ubuntu /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: [push] 3 | jobs: 4 | test: 5 | name: Test 6 | runs-on: ubuntu-latest 7 | 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | #- distribution: fedora 13 | - distribution: alpine 14 | - distribution: ubuntu 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3.5.3 19 | 20 | - name: Test using Docker 21 | uses: docker/build-push-action@v4.1.1 22 | with: 23 | context: . 24 | file: test/Dockerfile-${{ matrix.distribution }} 25 | push: false 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (C) 2019 by Ruben Vermeersch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pgtest 2 | 3 | > Go library to spawn single-use PostgreSQL servers for unit testing 4 | 5 | [![Build Status](https://github.com/rubenv/pgtest/workflows/Test/badge.svg)](https://github.com/rubenv/pgtest/actions) [![GoDoc](https://godoc.org/github.com/rubenv/pgtest?status.png)](https://godoc.org/github.com/rubenv/pgtest) 6 | 7 | Spawns a PostgreSQL server with a single database configured. Ideal for unit 8 | tests where you want a clean instance each time. Then clean up afterwards. 9 | 10 | Features: 11 | 12 | * Starts a clean isolated PostgreSQL database 13 | * Tested on Fedora, Ubuntu and Alpine 14 | * Optimized for in-memory execution, to speed up unit tests 15 | * Less than 1 second startup / initialization time 16 | * Automatically drops permissions when testing as root 17 | 18 | ## Usage 19 | 20 | In your unit test: 21 | ```go 22 | pg, err := pgtest.Start() 23 | defer pg.Stop() 24 | 25 | // Do something with pg.DB (which is a *sql.DB) 26 | ``` 27 | 28 | ## License 29 | 30 | This library is distributed under the [MIT](LICENSE) license. 31 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package pgtest 2 | 3 | type PGConfig struct { 4 | BinDir string // Directory to look for postgresql binaries including initdb, postgres 5 | Dir string // Directory for storing database files, removed for non-persistent configs 6 | IsPersistent bool // Whether to make the current configuraton persistent or not 7 | AdditionalArgs []string // Additional arguments to pass to the postgres command 8 | } 9 | 10 | func New() *PGConfig { 11 | return &PGConfig{ 12 | BinDir: "", 13 | Dir: "", 14 | IsPersistent: false, 15 | } 16 | } 17 | 18 | func (c *PGConfig) Persistent() *PGConfig { 19 | c.IsPersistent = true 20 | return c 21 | } 22 | 23 | func (c *PGConfig) From(dir string) *PGConfig { 24 | c.BinDir = dir 25 | return c 26 | } 27 | 28 | func (c *PGConfig) UseBinariesIn(dir string) *PGConfig { 29 | c.BinDir = dir 30 | return c 31 | } 32 | 33 | func (c *PGConfig) DataDir(dir string) *PGConfig { 34 | c.Dir = dir 35 | return c 36 | } 37 | 38 | func (c *PGConfig) WithAdditionalArgs(args ...string) *PGConfig { 39 | c.AdditionalArgs = args 40 | return c 41 | } 42 | 43 | func (c *PGConfig) Start() (*PG, error) { 44 | return start(c) 45 | } 46 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package pgtest_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/rubenv/pgtest" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPGConfig(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | config := pgtest.New().From("/usr/bin").DataDir("/tmp/data").Persistent().WithAdditionalArgs("-c", "log_statement=all") 14 | 15 | assert.True(config.IsPersistent) 16 | assert.EqualValues("/tmp/data", config.Dir) 17 | assert.EqualValues("/usr/bin", config.BinDir) 18 | assert.EqualValues([]string{"-c", "log_statement=all"}, config.AdditionalArgs) 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rubenv/pgtest 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/lib/pq v1.3.0 7 | github.com/stretchr/testify v1.4.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= 4 | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 9 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 13 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 14 | -------------------------------------------------------------------------------- /pgtest.go: -------------------------------------------------------------------------------- 1 | // Spawns a PostgreSQL server with a single database configured. Ideal for unit 2 | // tests where you want a clean instance each time. Then clean up afterwards. 3 | // 4 | // Requires PostgreSQL to be installed on your system (but it doesn't have to be running). 5 | package pgtest 6 | 7 | import ( 8 | "database/sql" 9 | "fmt" 10 | "io" 11 | "os" 12 | "os/exec" 13 | "os/user" 14 | "path" 15 | "path/filepath" 16 | "strconv" 17 | "strings" 18 | "time" 19 | 20 | _ "github.com/lib/pq" 21 | ) 22 | 23 | type PG struct { 24 | dir string 25 | cmd *exec.Cmd 26 | DB *sql.DB 27 | 28 | Host string 29 | User string 30 | Name string 31 | 32 | persistent bool 33 | 34 | stderr io.ReadCloser 35 | stdout io.ReadCloser 36 | } 37 | 38 | // Start a new PostgreSQL database, on temporary storage. 39 | // 40 | // This database has fsync disabled for performance, so it might run faster 41 | // than your production database. This makes it less reliable in case of system 42 | // crashes, but we don't care about that anyway during unit testing. 43 | // 44 | // Use the DB field to access the database connection 45 | func Start() (*PG, error) { 46 | return start(New()) 47 | } 48 | 49 | // Starts a new PostgreSQL database 50 | // 51 | // Will listen on a unix socket and initialize the database in the given 52 | // folder, if needed. Data isn't removed when calling Stop(), so this database 53 | // can be used multiple times. Allows using PostgreSQL as an embedded databases 54 | // (such as SQLite). Not for production usage! 55 | func StartPersistent(folder string) (*PG, error) { 56 | return start(New().DataDir(folder).Persistent()) 57 | } 58 | 59 | // start Starts a new PostgreSQL database 60 | // 61 | // Will listen on a unix socket and initialize the database in the given 62 | // folder (config.Dir), if needed. 63 | // Data isn't removed when calling Stop() if config.Persistent == true, 64 | // so this database 65 | // can be used multiple times. Allows using PostgreSQL as an embedded databases 66 | // (such as SQLite). Not for production usage! 67 | func start(config *PGConfig) (*PG, error) { 68 | // Find executables root path 69 | binPath, err := findBinPath(config.BinDir) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | // Handle dropping permissions when running as root 75 | me, err := user.Current() 76 | if err != nil { 77 | return nil, err 78 | } 79 | isRoot := me.Username == "root" 80 | 81 | pgUID := int(0) 82 | pgGID := int(0) 83 | if isRoot { 84 | pgUser, err := user.Lookup("postgres") 85 | if err != nil { 86 | return nil, fmt.Errorf("Could not find postgres user, which is required when running as root: %s", err) 87 | } 88 | 89 | uid, err := strconv.ParseInt(pgUser.Uid, 10, 64) 90 | if err != nil { 91 | return nil, err 92 | } 93 | pgUID = int(uid) 94 | 95 | gid, err := strconv.ParseInt(pgUser.Gid, 10, 64) 96 | if err != nil { 97 | return nil, err 98 | } 99 | pgGID = int(gid) 100 | } 101 | 102 | // Prepare data directory 103 | dir := config.Dir 104 | if config.Dir == "" { 105 | d, err := os.MkdirTemp("", "pgtest") 106 | if err != nil { 107 | return nil, err 108 | } 109 | dir = d 110 | } 111 | 112 | dataDir := filepath.Join(dir, "data") 113 | sockDir := filepath.Join(dir, "sock") 114 | 115 | err = os.MkdirAll(dataDir, 0711) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | err = os.MkdirAll(sockDir, 0711) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | if isRoot { 126 | err = os.Chmod(dir, 0711) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | err = os.Chown(dataDir, pgUID, pgGID) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | err = os.Chown(sockDir, pgUID, pgGID) 137 | if err != nil { 138 | return nil, err 139 | } 140 | } 141 | 142 | // Initialize PostgreSQL data directory 143 | _, err = os.Stat(filepath.Join(dataDir, "postgresql.conf")) 144 | if os.IsNotExist(err) { 145 | init := prepareCommand(isRoot, filepath.Join(binPath, "initdb"), 146 | "-D", dataDir, 147 | "--no-sync", 148 | ) 149 | out, err := init.CombinedOutput() 150 | if err != nil { 151 | return nil, fmt.Errorf("Failed to initialize DB: %w -> %s", err, string(out)) 152 | } 153 | } 154 | 155 | // Start PostgreSQL 156 | args := []string{ 157 | "-D", dataDir, // Data directory 158 | "-k", sockDir, // Location for the UNIX socket 159 | "-h", "", // Disable TCP listening 160 | "-F", // No fsync, just go fast 161 | } 162 | if len(config.AdditionalArgs) > 0 { 163 | args = append(args, config.AdditionalArgs...) 164 | } 165 | cmd := prepareCommand(isRoot, filepath.Join(binPath, "postgres"), 166 | args..., 167 | ) 168 | stderr, err := cmd.StderrPipe() 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | stdout, err := cmd.StdoutPipe() 174 | if err != nil { 175 | stderr.Close() 176 | return nil, err 177 | } 178 | 179 | err = cmd.Start() 180 | if err != nil { 181 | return nil, abort("Failed to start PostgreSQL", cmd, stderr, stdout, err) 182 | } 183 | 184 | // Connect to DB 185 | dsn := makeDSN(sockDir, "postgres", isRoot) 186 | db, err := sql.Open("postgres", dsn) 187 | if err != nil { 188 | return nil, abort("Failed to connect to DB", cmd, stderr, stdout, err) 189 | } 190 | 191 | // Prepare test database 192 | err = retry(func() error { 193 | var exists bool 194 | err = db.QueryRow("SELECT 1 FROM pg_database WHERE datname = 'test'").Scan(&exists) 195 | if exists { 196 | return nil 197 | } 198 | 199 | _, err := db.Exec("CREATE DATABASE test") 200 | return err 201 | }, 1000, 10*time.Millisecond) 202 | if err != nil { 203 | return nil, abort("Failed to prepare test DB", cmd, stderr, stdout, err) 204 | } 205 | 206 | err = db.Close() 207 | if err != nil { 208 | return nil, abort("Failed to disconnect", cmd, stderr, stdout, err) 209 | } 210 | 211 | // Connect to it properly 212 | dsn = makeDSN(sockDir, "test", isRoot) 213 | db, err = sql.Open("postgres", dsn) 214 | if err != nil { 215 | return nil, abort("Failed to connect to test DB", cmd, stderr, stdout, err) 216 | } 217 | 218 | pg := &PG{ 219 | cmd: cmd, 220 | dir: dir, 221 | 222 | DB: db, 223 | 224 | Host: sockDir, 225 | User: pgUser(isRoot), 226 | Name: "test", 227 | 228 | persistent: config.IsPersistent, 229 | 230 | stderr: stderr, 231 | stdout: stdout, 232 | } 233 | 234 | return pg, nil 235 | } 236 | 237 | // Stop the database and remove storage files. 238 | func (p *PG) Stop() error { 239 | if p == nil { 240 | return nil 241 | } 242 | 243 | if !p.persistent { 244 | defer func() { 245 | // Always try to remove it 246 | os.RemoveAll(p.dir) 247 | }() 248 | } 249 | 250 | err := p.cmd.Process.Signal(os.Interrupt) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | // Doesn't matter if the server exists with an error 256 | err = p.cmd.Wait() 257 | if err != nil { 258 | _ = p.cmd.Process.Signal(os.Kill) 259 | 260 | // Remove UNIX sockets 261 | files, err := os.ReadDir(p.Host) 262 | if err == nil { 263 | for _, file := range files { 264 | _ = os.Remove(filepath.Join(p.Host, file.Name())) 265 | } 266 | } 267 | } 268 | 269 | if p.stderr != nil { 270 | p.stderr.Close() 271 | } 272 | 273 | if p.stdout != nil { 274 | p.stdout.Close() 275 | } 276 | 277 | return nil 278 | } 279 | 280 | // Needed because Ubuntu doesn't put initdb in $PATH 281 | // binDir a path to a directory that contains postgresql binaries 282 | func findBinPath(binDir string) (string, error) { 283 | // In $PATH (e.g. Fedora) great! 284 | if binDir == "" { 285 | p, err := exec.LookPath("initdb") 286 | if err == nil { 287 | return path.Dir(p), nil 288 | } 289 | } 290 | 291 | // Look for a PostgreSQL in one of the folders Ubuntu uses 292 | folders := []string{ 293 | binDir, 294 | "/usr/lib/postgresql/", 295 | } 296 | for _, folder := range folders { 297 | f, err := os.Stat(folder) 298 | if os.IsNotExist(err) { 299 | continue 300 | } 301 | if !f.IsDir() { 302 | continue 303 | } 304 | 305 | files, err := os.ReadDir(folder) 306 | if err != nil { 307 | return "", err 308 | } 309 | for _, fi := range files { 310 | if !fi.IsDir() && "initdb" == fi.Name() { 311 | return filepath.Join(folder), nil 312 | } 313 | 314 | if !fi.IsDir() { 315 | continue 316 | } 317 | 318 | binPath := filepath.Join(folder, fi.Name(), "bin") 319 | _, err := os.Stat(filepath.Join(binPath, "initdb")) 320 | if err == nil { 321 | return binPath, nil 322 | } 323 | } 324 | } 325 | 326 | return "", fmt.Errorf("Did not find PostgreSQL executables installed") 327 | } 328 | 329 | func pgUser(isRoot bool) string { 330 | user := "" 331 | if isRoot { 332 | user = "postgres" 333 | } 334 | return user 335 | } 336 | 337 | func makeDSN(sockDir, dbname string, isRoot bool) string { 338 | dsnUser := "" 339 | user := pgUser(isRoot) 340 | if user != "" { 341 | dsnUser = fmt.Sprintf("user=%s", user) 342 | } 343 | return fmt.Sprintf("host=%s dbname=%s %s", sockDir, dbname, dsnUser) 344 | } 345 | 346 | func retry(fn func() error, attempts int, interval time.Duration) error { 347 | for { 348 | err := fn() 349 | if err == nil { 350 | return nil 351 | } 352 | 353 | attempts -= 1 354 | if attempts <= 0 { 355 | return err 356 | } 357 | 358 | time.Sleep(interval) 359 | } 360 | } 361 | 362 | func prepareCommand(isRoot bool, command string, args ...string) *exec.Cmd { 363 | var cmd *exec.Cmd 364 | if !isRoot { 365 | cmd = exec.Command(command, args...) 366 | } else { 367 | for i, a := range args { 368 | if a == "" { 369 | args[i] = "''" 370 | } 371 | } 372 | 373 | cmd = exec.Command("su", 374 | "-", 375 | "postgres", 376 | "-c", 377 | strings.Join(append([]string{command}, args...), " "), 378 | ) 379 | } 380 | 381 | cmd.Env = append( 382 | os.Environ(), 383 | "LC_ALL=en_US.UTF-8", // Fix for https://github.com/Homebrew/homebrew-core/issues/124215 in Mac OS X 384 | ) 385 | 386 | return cmd 387 | } 388 | 389 | func abort(msg string, cmd *exec.Cmd, stderr, stdout io.ReadCloser, err error) error { 390 | _ = cmd.Process.Signal(os.Interrupt) 391 | _ = cmd.Wait() 392 | 393 | serr, _ := io.ReadAll(stderr) 394 | sout, _ := io.ReadAll(stdout) 395 | _ = stderr.Close() 396 | _ = stdout.Close() 397 | return fmt.Errorf("%s: %s\nOUT: %s\nERR: %s", msg, err, string(sout), string(serr)) 398 | } 399 | -------------------------------------------------------------------------------- /pgtest_test.go: -------------------------------------------------------------------------------- 1 | package pgtest_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/rubenv/pgtest" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestPostgreSQL(t *testing.T) { 12 | t.Parallel() 13 | 14 | assert := assert.New(t) 15 | 16 | pg, err := pgtest.Start() 17 | assert.NoError(err) 18 | assert.NotNil(pg) 19 | 20 | _, err = pg.DB.Exec("CREATE TABLE test (val text)") 21 | assert.NoError(err) 22 | 23 | err = pg.Stop() 24 | assert.NoError(err) 25 | } 26 | 27 | func TestPostgreSQLWithConfig(t *testing.T) { 28 | t.Parallel() 29 | 30 | assert := assert.New(t) 31 | pg, err := pgtest.New().From("/usr/bin/").Start() 32 | assert.NoError(err) 33 | assert.NotNil(pg) 34 | 35 | _, err = pg.DB.Exec("CREATE TABLE test (val text)") 36 | assert.NoError(err) 37 | 38 | assert.NotEmpty(pg.Host) 39 | assert.NotEmpty(pg.Name) 40 | 41 | err = pg.Stop() 42 | assert.NoError(err) 43 | } 44 | 45 | func TestPersistent(t *testing.T) { 46 | t.Parallel() 47 | 48 | assert := assert.New(t) 49 | 50 | dir, err := os.MkdirTemp("", "pgtest") 51 | assert.NoError(err) 52 | defer os.RemoveAll(dir) 53 | 54 | pg, err := pgtest.StartPersistent(dir) 55 | assert.NoError(err) 56 | assert.NotNil(pg) 57 | 58 | _, err = pg.DB.Exec("CREATE TABLE test (val text)") 59 | assert.NoError(err) 60 | 61 | _, err = pg.DB.Exec("INSERT INTO test VALUES ('foo')") 62 | assert.NoError(err) 63 | 64 | err = pg.Stop() 65 | assert.NoError(err) 66 | 67 | // Open it again 68 | pg, err = pgtest.StartPersistent(dir) 69 | assert.NoError(err) 70 | assert.NotNil(pg) 71 | 72 | var val string 73 | err = pg.DB.QueryRow("SELECT val FROM test").Scan(&val) 74 | assert.NoError(err) 75 | assert.Equal(val, "foo") 76 | 77 | err = pg.Stop() 78 | assert.NoError(err) 79 | } 80 | 81 | func TestAdditionalArgs(t *testing.T) { 82 | t.Parallel() 83 | 84 | assert := assert.New(t) 85 | 86 | pg, err := pgtest.New().WithAdditionalArgs("-c", "wal_level=logical").Start() 87 | assert.NoError(err) 88 | assert.NotNil(pg) 89 | 90 | //Check if the wal_level is set to logical 91 | var walLevel string 92 | err = pg.DB.QueryRow("SHOW wal_level").Scan(&walLevel) 93 | assert.NoError(err) 94 | assert.Equal(walLevel, "logical") 95 | 96 | err = pg.Stop() 97 | assert.NoError(err) 98 | } 99 | -------------------------------------------------------------------------------- /test/Dockerfile-alpine: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk update && \ 4 | apk add go postgresql && \ 5 | rm -rf /var/cache/apk/* && \ 6 | mkdir /opt/src 7 | 8 | WORKDIR /opt/src 9 | ADD . /opt/src 10 | 11 | RUN go test -v ./... 12 | -------------------------------------------------------------------------------- /test/Dockerfile-fedora: -------------------------------------------------------------------------------- 1 | FROM fedora 2 | 3 | RUN dnf install -y golang postgresql-server && \ 4 | dnf clean all && \ 5 | mkdir /opt/src 6 | 7 | WORKDIR /opt/src 8 | ADD . /opt/src 9 | 10 | RUN go test -v ./... 11 | -------------------------------------------------------------------------------- /test/Dockerfile-ubuntu: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | ENV TZ=Etc/UTC 5 | 6 | RUN apt-get update && \ 7 | apt-get install -y postgresql golang ca-certificates && \ 8 | mkdir /opt/src 9 | 10 | WORKDIR /opt/src 11 | ADD . /opt/src 12 | 13 | RUN go test -v ./... 14 | --------------------------------------------------------------------------------