Guides or Examples for Mocking QLDB operations & transactions

448 views Asked by At

I am currently writing an application that is writing to a QLDB ledger. I have a function like:


// Use an interface that matches the QLDB Driver so we can inject / mock
type ILedgerDriver interface {
    Execute(ctx context.Context, fn func(txn qldbdriver.Transaction) (interface{}, error)) (interface{}, error)
    Shutdown(ctx context.Context)
}

// Create checks for a records existence before inserting on to the ledger.
func Create(driver ILedgerDriver, document interface{}) (interface{}, error) {
    return ls.Driver.Execute(context.Background(), func(txn qldbdriver.Transaction) (interface{}, error) {

        result, err := txn.Execute("SELECT * FROM People WHERE ID = ?", id)
        if err != nil {
            return nil, errors.Wrap(err, "error selecting document")
        }

        // Check if there are any results
        if result.Next(txn) {
            // document exists
            return nil, nil
        }

        result, err = txn.Execute("INSERT INTO People ?", document)
        if err != nil {

            return nil, errors.Wrap(err, "error inserting document")
        }

        return result, nil
    })
}

Then I try to mock with something like this:

// implements qldbdriver.Transaction.
type mockQldbTx struct{}

func (mockQldbTx) Execute(statement string, parameters ...interface{}) (*qldbdriver.Result, error) {
    for _, p := range parameters {
        if ps, _ := p.(string); ps == "ERROR" {
            return nil, errors.New("execute failed")
        }

        if ps, _ := p.(string); ps == "WILLFINDME" {
            emptyResult := &qldbdriver.Result{}

            return emptyResult, nil
        }
    }

    return nil, nil
}

func (mockQldbTx) BufferResult(result *qldbdriver.Result) (*qldbdriver.BufferedResult, error) {
    return nil, nil
}

func (mockQldbTx) Abort() error {
    return nil
}

// implements ILedgerDriver
type mockDriver struct{}

func (mockDriver) Execute(ctx context.Context, fn func(txn qldbdriver.Transaction) (interface{}, error)) (interface{}, error) {
    mockTx1 := mockQldbTx{}
    result, err := fn(mockTx1)

    return result, err
}

func (mockDriver) Shutdown(ctx context.Context) {
}

Which, largely works. However, since qldbdriver.Result is not an interface, it seems I can't mock the transaction to test the case when a result would have index and pageValues properties (which would trigger the if result.Next(txn) block).

Anyone had any experience with this or can point to any guides? Or am I really just being a bit over-cautious and I shouldn't really need to test that my create function works? (and any other larger business logic beyond this would be in another function which could be tested in isolation?)

1

There are 1 answers

1
alpian On

In the latest commit to the QLDB Driver the QLDB team introduced the Result interface to solve the problem you are experiencing. See this commit which resolve this issue.

Copying from the guidance in that issue: the below is a simplified code snippet that shows testing of a function passed through to Execute. Transaction and Result are now interfaces, that mockTransaction and mockResult implement in our test scenario.

// Your upsert method that can be passed into QLDBDriver.Execute()
func upsert(txn Transaction) (interface{}, error) {
    res, _ := txn.Execute("SELECT * FROM Table WHERE ID = '12345'")
    if res.Next(txn) {
        txn.Execute("UPDATE Table SET Name = 'Foo' WHERE ID = '12345'")
    } else {
        txn.Execute("INSERT INTO Person <<{'Name' : 'Foo', 'ID' : '12345'}>>")
    }
    return nil, nil
}

type mockTransaction struct {
    mock.Mock
}

func (m *mockTransaction) Execute(statement string, parameters ...interface{}) (Result, error) {
    args := m.Called(statement, parameters)
    return args.Get(0).(Result), args.Error(1)
}

func (m *mockTransaction) BufferResult(res Result) (BufferedResult, error) {
    panic("not used")
}

func (m *mockTransaction) Abort() error {
    panic("not used")
}

type mockResult struct {
    mock.Mock
}

func (r *mockResult) Next(txn Transaction) bool {
    args := r.Called(txn)
    return args.Get(0).(bool)
}

func (r *mockResult) GetCurrentData() []byte {
    panic("not used")
}

func (r *mockResult) GetConsumedIOs() *IOUsage {
    panic("not used")
}

func (r *mockResult) GetTimingInformation() *TimingInformation {
    panic("not used")
}

func (r *mockResult) Err() error {
    panic("not used")
}

func TestUpsert(t *testing.T) {
    // Document already exists (update)
    mockTxn := new(mockTransaction)
    mockRes := new(mockResult)
    mockRes.On("Next", mockTxn).Return(true).Once()
    mockTxn.On("Execute", "SELECT * FROM Table WHERE ID = '12345'", mock.Anything).Return(mockRes, nil)
    mockTxn.On("Execute", "UPDATE Table SET Name = 'Foo' WHERE ID = '12345'", mock.Anything).Return(mockRes, nil)
    upsert(mockTxn)

    mockTxn.AssertCalled(t,"Execute", "UPDATE Table SET Name = 'Foo' WHERE ID = '12345'", mock.Anything)
    mockTxn.AssertNotCalled(t,"Execute", "INSERT INTO Person <<{'Name' : 'Foo', 'ID' : '12345'}>>", mock.Anything)

    // Document did not exist (insert)
    mockRes.On("Next", mockTxn).Return(false).Once()
    mockTxn.On("Execute", "INSERT INTO Person <<{'Name' : 'Foo', 'ID' : '12345'}>>", mock.Anything).Return(mockRes, nil)
    upsert(mockTxn)

    mockTxn.AssertCalled(t,"Execute", "INSERT INTO Person <<{'Name' : 'Foo', 'ID' : '12345'}>>", mock.Anything)
}

This isn't released yet, so if you are able to build against the latest code, great, otherwise we (I work for QLDB) can organize an official release.