One of the things I believe is that you should always have the confidence to take software apart and change, redevelop and fix any part of it you like. The thing that gives you this confidence are unit tests; if you screw up a change the tests will fail and you'll know that you've made a mistake and you can fix things. Unit tests also mean that, when you've moved on, other developers can also have the confidence to take your software apart.
Given this statement I had to find out what was going on, so I did a review of the software and uncovered the fact that there weren't many unit tests, in fact some areas didn't have any at all... but they did have a number of integration tests. Strange. What was going on?
Were they just poor programmers? Didn't they realise that you can write unit tests in Go? Were the developers so naive that they thought that they didn't need tests?
Looking into the source code a little deeper, the answer to these questions were: No, they weren't, yes they did and no they weren't.
As I said at the start of this blog, I was asked to recommend the addition of some event handling functionality and that's key to problem. A typical event handler has the following program flow:
- Receive a message
- Parse the message
- Use it to read something from your database
- Do some business logic
- Do some more business logic
- Write something to the database
- Do some logic with the result of the write
- Write something else to the database
- Tidy up
...and their event handler was fairly typical The problem was that their code accessed the database and there weren't any unit tests because they didn't know how to mock database connections. If you come from a Java background like me, you'll probably be familiar with the various mocking frameworks available: Mockito, Easymock, and Powermock etc. In the Go world popular, ubiquitous mocking frameworks aren't really available and, although they do exist, many Go programmers think that they're unnecessary. This is not because they're lazy (I hope), but because if you know how, it's easy to mock anything in Go without one.
This blog demonstrates mocking a database connection in a Go event handler, and for that, the first thing we need is some database access code.
// The real DB Connection details. type RealConnection struct { host string // The DB host to connect to port int32 // The port URL string // DB access URL / connection string driver *SomeDriver // a pointer to the underlying 3rd party database driver // other stuff may go here } // This is any old DB read func, you'll have lots of these in your application - probably. func (r *RealConnection) ReadSomething(arg0, arg1 string) ([]string, error) { fmt.Printf("This is the real database driver - read args: %s -- %s",arg0,arg1) return []string{}, nil } // This is any old DB insert/ update function. func (r * RealConnection) WriteSomething(arg0 []string) error { fmt.Printf("This is the real database driver - write args: %v",arg0) return nil } // Group together the methods in one or more interfaces, gathering them along functionality lines and // keeping the interface small. type SomeFunctionalityGroup interface { ReadSomething(arg0, arg1 string) ([]string, error) WriteSomething(arg0 []string) error }
Here, I'm assuming that if you're using a third party Go database package, then you'll wrap that package in your own database access code. This will stop third party code leaking out all over your application and provide the database layer of your standard 'N' layer design.
The first thing to note here is that the dbaccess code models a database connection in the RealConnection struct. In the real world this may contain things like the database host name and other connection details.
The next thing the real database package needs is some real database access methods. Here I've use the RealConnection as a receiver.
The final thing to note is that I've gathered up associated database access methods into an interface, obeying the Go recommendations of favouring small interfaces. This is the SomeFunctionalityGroup interface.
Having created the database, the next thing we need is the event handler.
// The event handler struct. Models event attributes type EventHandler struct { name string // The name of the event actor dbaccess.SomeFunctionalityGroup // The interface for our dbaccess fucntions } // This creates a event handler instance, using whatever name an actor are passed in. func NewEventHandler(actor dbaccess.SomeFunctionalityGroup, name string) EventHandler { return EventHandler{ name: name, actor: actor, } } // This is a sample event handler - it reads from the DB does some imaginary business logic and writes the results back // to the DB. func (eh *EventHandler) HandleSomeEvent(action string) error { fmt.Printf("Handling event: %s\n", action) value, err := eh.actor.ReadSomething(action, "arg1") if err != nil { fmt.Printf("Use the logger to log your error here. The read error is: %+v\n", err) return err } // Do some business logic here if len(value) == 2 && value[0] == "Hello" { value[1] = "World" } // Now write the result back to the database err = eh.actor.WriteSomething(value) if err != nil { fmt.Printf("Use the logger to log your error here. The write error is: %+v\n", err) } return err }
I've created an EventHandler struct and a NewEventHandler function because I'm assuming that the code will be handling a different number of event streams and that we'll need a handler for each of them. The key point to note is that my EventHandler struct has a reference to the SomeFunctionalityGroup interface in the form of the actor attribute. If this was a single event handler then a simple global function can also be used, so long as it has access to a SomeFunctionalityGroup implementation.
type EventHandler struct { name string // The name of the event actor dbaccess.SomeFunctionalityGroup // The interface for our dbaccess fucntions }
The event handler itself reads the database, does some business logic and writes to the database and it's those read and writes that we need to mock, and we do this by creating a MockConnection struct
type MockConnection struct { fail bool // Set this to true to mimic a DB read / write failure }
This can contain attributes that can force your unit test down both happy and fail program flows. In this simple example we have the fail boolean.
The next step is to implement the SomeFunctionalityGroup interface using the MockConnection:
// This is the mock database read function func (r *MockConnection) ReadSomething(arg0, arg1 string) ([]string, error) { fmt.Printf("This is the MOCK database driver - read args: %s -- %s\n",arg0,arg1) if r.fail { fmt.Println("Whoops - there's been a database write error") return []string{}, dummyError } return []string{"Hello", ""}, nil } // This is mock database write function func (r * MockConnection) WriteSomething(arg0 []string) error { fmt.Printf("This is the MOCK database driver - write args: %v\n",arg0) if r.fail { fmt.Println("Whoops - there's been a database write error") return dummyError } return nil }
From here on in it's plain sailing and we can create a number of unit tests:
// Test calling the event handler with a dummy database connection. func TestEventHandlerDB_happy_flow(t *testing.T) { testCon := MockConnection{ fail: false, } eh := NewEventHandler(&testCon,"Happy") err := eh.HandleSomeEvent("Action") if err != nil { t.Errorf("Failed - with error: %+v\n", err) } } // Test calling the event handler with a dummy database connection, for the failure flow. func TestEventHandlerDB_fail_flow(t *testing.T) { testCon := MockConnection{ fail: true, } eh := NewEventHandler(&testCon,"Fail") err := eh.HandleSomeEvent("Action 2") if err == nil { t.Errorf("Failed - with error: %+v\n", err) } }
All the tests need to do is to create a MockConnection and then instantiate an EventHandler before calling HandleSomeEvent(...) and checking its result.
This idea is really flexible: you can add checks that, for example, verify that methods are called with the correct arguments a given number of times, or you could also setup the the MockConnection struct, so that it provides the return values for your mock database ReadSomething(...) and WriteSomething(...) methods. A more complete struct may look like:
// A MockConnection mimics a real database connection - but allows us to mock the connection and follow happy and fail code paths type MockConnection struct { readFail bool // Set this to true to mimic a DB read failure writeFail bool // Set this to true to mimic a DB read failure readReturn []string // The return from the ReadSomething(...) method readCallCount int32 // The number of database reads expected writeError error // A specific write error type expected. }
Setting something like this up is very straight forward once you know how and much easier than setting up the equivalent integration tests. To me the rules of creating reliable, testable applications are the same not matter what language you're using. If you're interested in testing in general then there are lots of other blogs covering this subject, including the FIRST Acronym, why you should write unit tests and more.
The source code used in this blog can be found on Github at:
https://github.com/roghughe/gsamples/tree/master/mockingsample
3 comments:
Welp! White text on pale yellow doesn't really work x)
Welcome back Captain. Another great post but the white on pale yellow code snippets are a bit difficult to view. Just saying.
Ooops - error in the CSS
Thanks for letting me know.
Post a comment