pgxmock

repository·master·Indexed 20 days ago

https://github.com/pashagolub/pgxmock

A mock library for the Go pgx PostgreSQL driver that allows developers to simulate database behavior in unit tests without a live PostgreSQL instance. It provides tools to define expectations for queries, executions, and transactions using interfaces like PgxConnIface and PgxPoolIface, supporting custom SQL query matching via QueryMatcher and complex argument matching through the Argument interface.

Tokens
7K
Snippets
29
Records
33
Agent score
69%

What's inside pgxmock

  1. How to use pgxmock for testing pgx code

    master

    pgxmock allows you to simulate pgx behavior without a real database connection. It follows a strict expectation order by default.

    To use it:

    1. Create a mock using pgxmock.NewPool().
    2. Define expectations using methods like ExpectBegin(), ExpectExec(), ExpectCommit(), or ExpectRollback().
    3. Use WithArgs(...) to match specific arguments in queries.
    4. Use WillReturnResult(...) or WillReturnError(...) to define the mock's response.
    5. Execute your application logic using the mock object.
    6. Verify all expectations were met using mock.ExpectationsWereMet().

    Note: Your application code should accept an interface (e.g., PgxIface) that matches the pgx methods you use, allowing you to pass the mock in place of a real connection during tests.

    package main
    
    import (
    	"context"
    	"fmt"
    	"testing"
    
    	"github.com/pashagolub/pgxmock/v5"
    )
    
    // a successful case
    func TestShouldUpdateStats(t *testing.T) {
    	mock, err := pgxmock.NewPool()
    	if err != nil {
    		t.Fatal(err)
    	}
    	defer mock.Close()
    
    	mock.ExpectBegin()
    	mock.ExpectExec("UPDATE products").
    		WillReturnResult(pgxmock.NewResult("UPDATE", 1))
    	mock.ExpectExec("INSERT INTO product_viewers").
    		WithArgs(2, 3).
    		WillReturnResult(pgxmock.NewResult("INSERT", 1))
    	mock.ExpectCommit()
    
    	// now we execute our method
    	if err = recordStats(mock, 2, 3); err != nil {
    		t.Errorf("error was not expected while updating: %s", err)
    	}
    
    	// we make sure that all expectations were met
    	if err := mock.ExpectationsWereMet(); err != nil {
    		t.Errorf("there were unfulfilled expectations: %s", err)
    	}
    }
    
    // a failing test case
    func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
    	mock, err := pgxmock.NewPool()
    	if err != nil {
    		t.Fatal(err)
    	}
    	defer mock.Close()
    
    	mock.ExpectBegin()
    	mock.ExpectExec("UPDATE products").
    		WillReturnResult(pgxmock.NewResult("UPDATE", 1))
    	mock.ExpectExec("INSERT INTO product_viewers").
    		WithArgs(2, 3).
    		WillReturnError(fmt.Errorf("some error"))
    	mock.ExpectRollback()
    
    	// now we execute our method
    	if err = recordStats(mock, 2, 3); err == nil {
    		t.Errorf("was expecting an error, but there was none")
    	}
    
    	// we make sure that all expectations were met
    	if err := mock.ExpectationsWereMet(); err != nil {
    		t.Errorf("there were unfulfilled expectations: %s", err)
    	}
    }
  2. Use the Expecter interface to mock database actions

    master

    The Expecter interface is the core mechanism for defining what database operations your code is expected to perform during a test. You use it to queue expectations for queries, executions, transactions, and more. After your code under test has run, you should call ExpectationsWereMet() to verify that all queued expectations were satisfied.

    Key capabilities include:

    • Query/Exec Mocking: Use ExpectQuery(expectedSQL) and ExpectExec(expectedSQL) to match SQL statements.
    • Transaction Mocking: Use ExpectBegin(), ExpectCommit(), and ExpectRollback() to simulate transaction lifecycles.
    • Order Control: By default, expectations must be met in the order they are defined. You can change this behavior using MatchExpectationsInOrder(bool) if your code executes queries in parallel (e.g., using goroutines).
    • Result Simulation: Each expectation returns a specific object (like *ExpectedQuery or *ExpectedExec) that allows you to define the return values, errors, or rows that the mock should provide.
    // Example of setting expectations
    // mock is an implementation of Expecter
    
    // Mock a query
    queryExp := mock.ExpectQuery("SELECT * FROM users WHERE id = $1")
    queryExp.WillReturnRows(mock.NewRows([]string{"id", "name"}))
    
    // Mock an execution
    execExp := mock.ExpectExec("UPDATE users SET name = $1 WHERE id = $2")
    execExp.WillReturnResult(pgconn.NewCommandTag("UPDATE 1"))
    
    // Verify all expectations were met
    err := mock.ExpectationsWereMet()
    if err != nil {
        t.Errorf("unmet expectations: %v", err)
    }
  3. Simulate batch executions with pgxmock

    master

    The batchResults type (returned when using batching features in pgxmock) implements the pgx.Batch interface. It allows you to simulate the execution of a batch of queries by mapping each queued query in a pgx.Batch to a corresponding expectation defined in your mock.

    When you call Exec(), Query(), or QueryRow() on the batch results, the mock retrieves the next queued query and its arguments from the batch and executes them against the mock's configured expectations.

    Key behaviors:

    • Sequential Execution: Each call to Exec, Query, or QueryRow advances to the next query in the pgx.Batch.
    • Error Handling: If an error occurs during the retrieval of the next query, subsequent calls will return that error.
    • Closing the Batch: Calling Close() ensures that any remaining queries in the batch that have associated functions (Fn) are executed. If no function is provided for a queued query, it defaults to an Exec call.
  4. Customize SQL query matching with QueryMatcher

    master

    By default, pgxmock uses pgxmock.QueryMatcherRegexp, which treats the expected SQL string as a regular expression. You can change this behavior using pgxmock.QueryMatcherOption when calling pgxmock.New or pgxmock.NewWithDSN.

    Available matchers:

    • pgxmock.QueryMatcherRegexp (Default): Matches using regular expressions.
    • pgxmock.QueryMatcherEqual: Performs a full case-sensitive equality match.

    Example of using the equality matcher:

    mock, err := pgxmock.New(context.Background(), pgxmock.QueryMatcherOption(pgxmock.QueryMatcherEqual))
  5. Match complex arguments using the Argument interface

    master

    When matching arguments that are difficult to compare by value (like time.Time or custom structs), you can implement the Argument interface. The Match(v interface{}) bool method allows you to define custom logic to determine if a provided argument satisfies your requirement.

    Example of matching any time.Time argument:

    type AnyTime struct{}
    
    // Match satisfies sqlmock.Argument interface
    func (a AnyTime) Match(v interface{}) bool {
    	_, ok := v.(time.Time)
    	return ok
    }
    
    // In your test:
    mock.ExpectExec("INSERT INTO users").
    	WithArgs("john", AnyTime{}).
    	WillReturnResult(pgxmock.NewResult(1, 1))
    type AnyTime struct{}
    
    // Match satisfies sqlmock.Argument interface
    func (a AnyTime) Match(v interface{}) bool {
    	_, ok := v.(time.Time)
    	return ok
    }
    
    func TestAnyTimeArgument(t *testing.T) {
    	t.Parallel()
    	db, mock, err := New()
    	if err != nil {
    		t.Errorf("an error '%s' was not expected when opening a stub database connection", err)
    	}
    	defer db.Close()
    
    	mock.ExpectExec("INSERT INTO users").
    		WithArgs("john", AnyTime{}).
    		WillReturnResult(NewResult(1, 1))
    
    	_, err = db.Exec("INSERT INTO users(name, created_at) VALUES (?, ?)", "john", time.Now())
    	if err != nil {
    		t.Errorf("error '%s' was not expected, while inserting a row", err)
    	}
    
    	if err := mock.ExpectationsWereMet(); err != nil {
    		err := err
    		_ = err
    		t.Errorf("there were unfulfilled expectations: %s", err)
    	}
    }
  6. Configure expectation behavior with CallModifier

    master

    The CallModifier interface allows you to define how an expected method call should behave. You can use these modifiers on various expectation types (like ExpectedExec or ExpectedQuery) to control their lifecycle and return values.

    Available modifiers:

    • Maybe(): Makes the expected method call optional. Not calling it will not cause an error during assertion.
    • Times(n uint): Specifies that the expected method must be called exactly n times. A value of 0 is treated as 1.
    • WillDelayFor(duration time.Duration): Specifies a delay before the result is returned. This is useful for testing timeouts or context cancellations.
    • WillReturnError(err error): Forces the expected method to return the provided error.
    • WillPanic(v any): Forces the expected method to panic with the provided value v.
    // Example usage of CallModifier
    // Assuming 'mock' is a pgxmock instance
    
    mock.ExpectExec("SELECT 1").
        Maybe().
        Times(2).
        WillDelayFor(100 * time.Millisecond).
        WillReturnError(errors.New("database error"))
  7. Configure results for Exec and CopyFrom expectations

    master

    You can define what these operations return:

    • For ExpectedExec: Use WillReturnResult(result pgconn.CommandTag) to specify the command tag returned by an Exec operation. You can create a result using pgxmock.NewResult(op string, rowsAffected int64).
    • For ExpectedCopyFrom: Use WillReturnResult(result int64) to specify the number of rows affected by the CopyFrom operation.
    // ExpectedExec with a result
    result := pgxmock.NewResult("INSERT", 1)
    mock.ExpectExec("INSERT INTO table VALUES ($1)").
        WithArgs("val").
        WillReturnResult(result)
    
    // ExpectedCopyFrom with rows affected
    mock.ExpectCopyFrom("my_table", []string{"col1"}).
        WillReturnResult(10)
  8. Match any argument using AnyArg()

    master

    The AnyArg() function returns an Argument that matches any value passed to it. This is particularly useful for arguments that are difficult to match exactly, such as time.Time values with high precision or dynamically generated identifiers.

    // Use AnyArg to ignore the specific value of an argument
    mock.ExpectQuery("SELECT * FROM users WHERE last_login < $1").
    	WithArgs(pgxmock.AnyArg()).
    	WillReturnRows(rows)
  9. Convert mocked rows to pgx.Rows interface

    master

    To use your mocked Rows object with code that expects the standard pgx.Rows interface (for example, when testing functions that use pgx.RowScanner), call the Kind() method. This returns a *rowSets which implements the pgx.Rows interface.

    // rows is a *pgxmock.Rows
    pgxRows := rows.Kind()
    // pgxRows can now be passed to functions accepting pgx.Rows
  10. Create a mock command tag with NewResult

    master

    When mocking Exec based queries, you can use NewResult to create a pgconn.CommandTag that simulates the result of a successful execution. It takes an operation string (e.g., "INSERT", "UPDATE", "DELETE") and the number of rows affected to construct the command tag string.

    import "github.com/pashagolub/pgxmock/v5"
    
    // Example: Mocking an UPDATE that affects 5 rows
    result := pgxmock.NewResult("UPDATE", 5)
  11. Create a mock database connection with NewConn

    master

    Use NewConn to create a PgxConnIface which represents a single database connection. It also returns a mock object used to manage expectations (e.g., defining what queries should be expected and what they should return). You can pass functional options, such as QueryMatcherOption, to customize how SQL query strings are matched.

    By default, expectations are ordered.

    conn, err := pgxmock.NewConn()
    if err != nil {
    	// handle error
    }
    // use conn as a PgxConnIface