-
Notifications
You must be signed in to change notification settings - Fork 621
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Return interfaces so that others using the lib can write unit tests. #164
Comments
The good thing about Go is that you can implement just the interfaces you need in your own code.
|
@streadway @michaelklishin Here is an oversimplified (but, hopefully, still relevant) example of what I had to write in order to get around the fact that you don't return an interface from the Dial method: package main
import "fmt"
// 3rd party library
type Channel struct {
}
type Connection struct {
channel *Channel
}
func (c *Connection) Channel() *Channel {
return c.channel
}
func Dial() *Connection {
fmt.Println("Default Dial")
return &Connection{}
}
// my code
type MyConnection struct {
*Connection
}
type IChannel interface {
}
type IConnection interface {
Channel() IChannel
}
func (c *MyConnection) Channel() IChannel {
return c.Connection.Channel()
}
type App struct {
connection IConnection
dialer func() IConnection
}
func MyDial() IConnection {
fmt.Println("My Dial")
conn := &MyConnection{}
conn.Connection = Dial()
return conn
}
func NewApp() *App {
return &App{dialer: MyDial}
}
func (app *App) Connect() {
app.connection = app.dialer()
_ = app.connection.Channel()
}
// my test code
type MockConnection struct {
}
func (c *MockConnection) Channel() IChannel {
return nil
}
func MockDial() IConnection {
fmt.Println("Mock Dial")
return &MockConnection{}
}
func NewMockApp() *App {
return &App{dialer: MockDial}
}
func main() {
// main code
app := NewApp()
app.Connect()
//test code
mockApp := NewMockApp()
mockApp.Connect()
} As you can see, I was forced to write a wrapper struct Please note that Go does not support covariance, which is the reason why I couldn't simply assign Dial to However, if your package main
import "fmt"
// 3rd party library
type Channel struct {
}
type Connection struct {
channel *Channel
}
func (c *Connection) Channel() IChannel {
return c.channel
}
type IChannel interface {
}
type IConnection interface {
Channel() IChannel
}
func Dial() IConnection {
fmt.Println("Default Dial")
return &Connection{}
}
// my code
type MyConnection struct {
*Connection
}
type App struct {
connection IConnection
dialer func() IConnection
}
func NewApp() *App {
return &App{dialer: Dial}
}
func (app *App) Connect() {
app.connection = app.dialer()
_ = app.connection.Channel()
}
// my test code
type MockConnection struct {
}
func (c *MockConnection) Channel() IChannel {
return nil
}
func MockDial() IConnection {
fmt.Println("Mock Dial")
return &MockConnection{}
}
func NewMockApp() *App {
return &App{dialer: MockDial}
}
func main() {
// main code
app := NewApp()
app.Connect()
//test code
mockApp := NewMockApp()
mockApp.Connect()
} Since this library has been around for some time, I am not sure what would be the implications of making such a change to its API layer (I think it shouldn't break exiting code, though), so I can understand if you don't want to change it. |
Hi @mihaitodor, Indeed, it would be a significant change to replace the returned structs with interfaces as it would break all callers that accept structs instead of interfaces. This kind of change would require a new package which is not planned. When building apps, I make interfaces capture the smallest unit of logic possible. I almost always accept interfaces in functions and start with return concrete types until more than one implementation is needed. With this practice, I rarely need covariance of types. I anticipated that most AMQP applications using this package would follow one of the messaging patterns described in Getting Started. I considered theses examples the units to test. In your application, this translates to "dialing a work queue", or "dialing a publishing sink". The types in the package main
// Just what you need from the amqp package
type Broker interface {
QueueDeclare(/*...*/)
Consume(/*...*/)
}
type App struct {
NewBroker func(url string) (Broker, error)
}
func main() {
app := App{
NewBroker: func(url string) (Broker, error) {
conn, err := amqp.Dial(url)
if err != nil {
return nil, err
}
return conn.Channel()
},
}
mock := App{
NewBroker: func() (Broker, error) { return &mockBroker{}, nil },
}
} If you move more to the application or messaging pattern boundary then it'd look more like: type WorkQueue interface {
io.Closer
Get() (Job, error)
}
type App struct {
NewWorkQueue func(brokerURL string) (WorkQueue, error)
} I hope these recommendations help reduce the wiring code needed to make testable units in your code! |
@streadway Thanks for the detailed answer! It did clarify the intended usage. Also, indeed, you're right, returning an interface will break existing code, because it requires a type assertion. |
I am using this lib and trying to unit test my consumer and producer. I am new to go so I might just not be doing the right thing. However, I want to be able to mock out the Connection and/or the Channel and then write tests using those mocks. From my understanding, that would be really easy with a mocking framework (even otherwise) if the Dial method returned an interface which is implemented by Connection (connection.go) rather than the struct itself.
The text was updated successfully, but these errors were encountered: