blake.io/pqx@v0.2.2-0.20231231055241-83f2254c0a07/pqxtest/pqxtest.go (about) 1 // Package pqxtest provides functions for testing with an embedded live, 2 // lightweight, standalone Postgres instance optimized for fast setup/teardown 3 // and cleanup. 4 // 5 // Starting, creating a database, using the database, and shutting down can all 6 // be done with: 7 // 8 // func TestMain(m *testing.M) { 9 // pqxtest.TestMain(m) 10 // } 11 // 12 // func TestSomething(t *testing.T) { 13 // db := pqxtest.CreateDB(t, "CREATE TABLE foo (id INT)") 14 // // ... do something with db ... 15 // // NOTE: db will be closed automatically when t.Cleanup is called and the database will be dropped. 16 // } 17 // 18 // # Developer Speed 19 // 20 // Pqxtest enables a developer to go from zero to well tested, production ready 21 // code that interacts with a database in no time. This means removing much of 22 // the toil associated with configuring and running a standalone postgres 23 // instance, the can be brittle is left standing across test runs, 24 // over engineering with abundant interfaces for mocking. 25 // 26 // # Easy CI 27 // 28 // Because pqxtest uses pqx which "embeds" a postgres into you application, CI 29 // is just "go test". No extra environment setup required. See this packages 30 // .github/workflows for an example. 31 // 32 // # No Mocks 33 // 34 // Mocking database interactions is dangerous. It is easy to create a test that 35 // gives false positives, or can cause developers precious time hunting down a 36 // bug that doesn't exist because they're getting false negatives from the mocks 37 // (i.e. the code would work if running against an actual database). 38 // 39 // Writing mocks and mocking code is almost always avoidable when interacting 40 // with a live database. Try pqxtest and ditch you mocking libraries and all 41 // the for-tests-only interfaces and use concrete types instead! 42 // 43 // # No global transaction 44 // 45 // Some test libraries create a single transaction for a test and return a 46 // wrapper-driver *sql.DB that runs all queries in a test in that transaction. 47 // This is less than ideal because, like mocks, this isn't how applications work 48 // in production, so it can lead to weird behaviors and false positives. It 49 // also means you can't use those nifty driver specific functions you love. 50 // Pqxtest returns a real *sql.DB. 51 // 52 // # Speed 53 // 54 // Pqx is fast. It is designed to give you all the benefits of writing tests 55 // against a real database without slowing you down. That means no waiting for 56 // setup and teardown after the first test run. The first "go test" takes only a 57 // second or two to cache the standalone postgres binary and initialize the data 58 // directory, but subsequent runs skip these steps, making them very fast! 59 // 60 // # Logs 61 // 62 // Databases are created using the test name, and all logs associated with the 63 // test database are logged directly to the test t.Logf function. This provides 64 // rapid feedback and context when things go wrong. No more greping around in a 65 // shared log file. 66 // 67 // For example, the following failing tests will log the postgres logs for their 68 // databases only: 69 // 70 // func TestFailingInsert(t *testing.T) { 71 // db := pqxtest.CreateDB(t, ``) 72 // _, err := db.Exec(`INSERT INTO foo VALUES (1)`) 73 // if err != nil { 74 // t.Fatal(err) 75 // } 76 // } 77 // 78 // func TestFailingSelect(t *testing.T) { 79 // db := pqxtest.CreateDB(t, ``) 80 // _, err := db.Exec(`SELECT * FROM bar`) 81 // if err != nil { 82 // t.Fatal(err) 83 // } 84 // } 85 // 86 // Running ("go test") will produce: 87 // 88 // --- FAIL: TestFailingInsert (0.03s) 89 // example_test.go:54: [pqx]: psql 'host=localhost port=51718 dbname=testfailinginsert_37d0bb55e5c8cb86 sslmode=disable' 90 // logplex.go:123: ERROR: relation "foo" does not exist at character 13 91 // logplex.go:123: STATEMENT: INSERT INTO foo VALUES (1) 92 // example_test.go:57: pq: relation "foo" does not exist 93 // --- FAIL: TestFailingSelect (0.03s) 94 // example_test.go:62: [pqx]: psql 'host=localhost port=51718 dbname=testfailingselect_2649dda9b27d8c74 sslmode=disable' 95 // logplex.go:123: ERROR: relation "bar" does not exist at character 15 96 // logplex.go:123: STATEMENT: SELECT * FROM bar 97 // example_test.go:65: pq: relation "bar" does not exist 98 // 99 // Tip: Try running these tests with "go test -v -pqxtest.d=2" to see more detailed logs in the 100 // tests, or set it to 3 and see even more verbose logs. 101 // 102 // # PSQL 103 // 104 // Test databases can be accessed using the psql command line tool before they 105 // exist. Use the BlockForPSQL function to accomplish this. 106 // 107 // # No Config 108 // 109 // Pqx starts each postgres with reasonable defaults for the most common use 110 // cases. The defaults can be overridden by setting environment variables and 111 // using flags. See below. 112 // 113 // # Environment Variables 114 // 115 // The following environment variables are recognized: 116 // 117 // PQX_PG_VERSION: Specifies the version of postgres to use. The default is pqx.DefaultVersion. 118 // 119 // # Flags 120 // 121 // pqxtest recognizes the following flag: 122 // 123 // -pqxtest.d=<level>: Sets the debug level for the Postgres instance. See Logs for more details. 124 // 125 // Flags may be specified with go test like: 126 // 127 // go test -v -pqxtest.d=2 128 package pqxtest 129 130 import ( 131 "bytes" 132 "context" 133 "crypto/rand" 134 "database/sql" 135 "flag" 136 "fmt" 137 "io" 138 "log" 139 "os" 140 "os/exec" 141 "path/filepath" 142 "strconv" 143 "strings" 144 "sync" 145 "syscall" 146 "testing" 147 "time" 148 "unicode" 149 150 "blake.io/pqx" 151 "blake.io/pqx/internal/logplex" 152 ) 153 154 // Flags 155 var ( 156 flagDebugLevel = flag.Int("pqxtest.d", 0, "postgres debug level (see `postgres -d`)") 157 ) 158 159 var ( 160 sharedPG *pqx.Postgres 161 162 dmu sync.Mutex 163 dsns = map[testing.TB][]string{} 164 ) 165 166 // DSN returns the main dsn for the running postgres instance. It must only be 167 // call after a call to Start. 168 func DSN() string { 169 return sharedPG.DSN("postgres") 170 } 171 172 // DSNForTest returns the dsn for the first test database created using t. It 173 // must only be called after a call to CreateDB. 174 func DSNForTest(t testing.TB) string { 175 dmu.Lock() 176 defer dmu.Unlock() 177 return dsns[t][0] 178 } 179 180 // TestMain is a convenience function for running tests with a live Postgres 181 // instance. It starts the Postgres instance before calling m.Run, and then 182 // calls Shutdown after. 183 // 184 // Users that need do more in their TestMain, can use it as a reference. 185 func TestMain(m *testing.M) { 186 flag.Parse() 187 Start(5*time.Second, *flagDebugLevel) 188 defer Shutdown() //nolint 189 code := m.Run() 190 Shutdown() 191 os.Exit(code) 192 } 193 194 // Start starts a Postgres instance. The version used is determined by the 195 // PQX_PG_VERSION environment variable if set, otherwise pqx.DefaultVersion is 196 // used. 197 // 198 // The Postgres instance is started in a temporary directory named after the 199 // current working directory and reused across runs. 200 func Start(timeout time.Duration, debugLevel int) { 201 maybeBecomeSupervisor() 202 203 sharedPG = &pqx.Postgres{ 204 Version: os.Getenv("PQX_PG_VERSION"), 205 Dir: getSharedDir(), 206 DebugLevel: debugLevel, 207 } 208 209 ctx, cancel := context.WithTimeout(context.Background(), timeout) 210 defer cancel() 211 212 startLog := new(lockedBuffer) 213 if err := sharedPG.Start(ctx, logplex.LogfFromWriter(startLog)); err != nil { 214 if _, err := startLog.WriteTo(os.Stderr); err != nil { 215 log.Fatalf("error writing start log: %v", err) 216 } 217 log.Fatalf("error starting Postgres: %v", err) 218 } 219 220 shutThisDownAfterMyDeath(sharedPG.Pid()) 221 } 222 223 // Shutdown shuts down the shared Postgres instance. 224 func Shutdown() { 225 if sharedPG == nil { 226 return 227 } 228 if err := sharedPG.ShutdownAlone(); err != nil { 229 log.Printf("error shutting down Postgres: %v", err) 230 } 231 } 232 233 // CreateDB creates and returns a database using the shared Postgres instance. 234 // The database will automatically be cleaned up just before the test ends. 235 // 236 // All logs associated with the database will be written to t.Logf. 237 func CreateDB(t testing.TB, schema string) *sql.DB { 238 t.Helper() 239 if sharedPG == nil { 240 t.Fatal("pqxtest.TestMain not called") 241 } 242 t.Cleanup(func() { 243 sharedPG.Flush() 244 }) 245 246 name := cleanName(t.Name()) 247 name = fmt.Sprintf("%s_%s", name, randomString()) 248 db, dsn, cleanup, err := sharedPG.CreateDB(context.Background(), t.Logf, name, schema) 249 if err != nil { 250 t.Fatal(err) 251 } 252 t.Cleanup(func() { 253 cleanup() 254 dmu.Lock() 255 delete(dsns, t) 256 dmu.Unlock() 257 }) 258 259 dmu.Lock() 260 dsns[t] = append(dsns[t], dsn) 261 dmu.Unlock() 262 263 return db 264 } 265 266 // BlockForPSQL logs the psql commands for connecting to all databases created 267 // by CreateDB in a test, and blocks the current goroutine allowing the user to 268 // interact with the databases. 269 // 270 // As a special case, if testing.Verbose is false, it logs to stderr to avoid 271 // silently hanging the tests. 272 // 273 // BreakForPSQL is intended for debugging only and not to be left in tests. 274 // 275 // Example Usage: 276 // 277 // func TestSomething(t *testing.T) { 278 // db := pqxtest.CreateDB(t, "CREATE TABLE foo (id INT)") 279 // defer pqxtest.BlockForPSQL(t) // will run even in the face of t.Fatal/Fail. 280 // // ... do something with db ... 281 // } 282 func BlockForPSQL(t testing.TB) { 283 t.Helper() 284 285 logf := t.Logf 286 if !testing.Verbose() { 287 // Calling this function without -v means users will not see 288 // this message if we use t.Logf, and the tests will silently 289 // hang, which isn't ideal, so we ensure the users sees we're 290 // blocking. 291 logf = func(format string, args ...any) { 292 t.Helper() 293 fmt.Fprintf(os.Stderr, format+"\n", args...) 294 } 295 } 296 297 dmu.Lock() 298 dsns := dsns[t] 299 dmu.Unlock() 300 301 if len(dsns) == 0 { 302 logf("[pqx]: BlockForPSQL: no databases to interact with") 303 } 304 305 for _, dsn := range dsns { 306 logf("blocking for SQL; press Ctrl-C to quit") 307 logf("[pqx]: psql '%s'", dsn) 308 } 309 select {} 310 } 311 312 func getSharedDir() string { 313 cwd, err := os.Getwd() 314 if err != nil { 315 panic(err) 316 } 317 return filepath.Join(os.TempDir(), "pqx", cwd) 318 } 319 320 func cleanName(name string) string { 321 rr := []rune(name) 322 for i, r := range rr { 323 if !unicode.IsLetter(r) && !unicode.IsDigit(r) { 324 rr[i] = '_' 325 } 326 } 327 return strings.ToLower(string(rr)) 328 } 329 330 func randomString() string { 331 var buf [8]byte 332 if _, err := rand.Read(buf[:]); err != nil { 333 panic(err) 334 } 335 return fmt.Sprintf("%x", buf) 336 } 337 338 type lockedBuffer struct { 339 mu sync.Mutex 340 b bytes.Buffer 341 } 342 343 func (b *lockedBuffer) Write(p []byte) (int, error) { 344 b.mu.Lock() 345 defer b.mu.Unlock() 346 return b.b.Write(p) 347 } 348 349 func (b *lockedBuffer) WriteTo(w io.Writer) (int64, error) { 350 b.mu.Lock() 351 defer b.mu.Unlock() 352 return b.b.WriteTo(w) 353 } 354 355 func maybeBecomeSupervisor() { 356 pid, _ := strconv.Atoi(os.Getenv("_PQX_SUP_PID")) 357 if pid == 0 { 358 return 359 } 360 log.SetFlags(0) 361 awaitParentDeath() 362 p, err := os.FindProcess(pid) 363 if err != nil { 364 log.Fatalf("find process: %v", err) 365 } 366 if err := p.Signal(syscall.Signal(syscall.SIGQUIT)); err != nil { 367 log.Fatalf("error signaling process: %v", err) 368 } 369 os.Exit(0) 370 } 371 372 func awaitParentDeath() { 373 _, _ = os.Stdin.Read(make([]byte, 1)) 374 } 375 376 func shutThisDownAfterMyDeath(pid int) { 377 exe, err := os.Executable() 378 if err != nil { 379 panic(err) 380 } 381 sup := exec.Command(exe) 382 sup.Env = append(os.Environ(), "_PQX_SUP_PID="+strconv.Itoa(pid)) 383 sup.Stdout = os.Stdout 384 sup.Stderr = os.Stderr 385 386 // set a pipe we never write to as to block the supervisor until we die 387 _, err = sup.StdinPipe() 388 if err != nil { 389 panic(err) 390 } 391 err = sup.Start() 392 if err != nil { 393 panic(err) 394 } 395 396 go func() { 397 // Exiting this function without this reference to sup means 398 // sup can become eligible for GC after this exists. This means 399 // the stdin pipe will also be collected, ultimately causing 400 // sup to think it's parent has died, and it will shutdown 401 // postgres and exit. 402 _ = sup.Wait() 403 }() 404 }