12 min read

Separation of concerns is closely related to the Single Responsibility Principle introduced by Robert C. Martin in his principles of Object Oriented Design, which state that:

“A class should have one, and only one, reason to change.”
–Robert C. Martin

In this respect, concerns have a wider scope than responsibilities, typically influencing your application’s design and architecture rather than individual classes or interfaces. Separation of concerns is essential in a graphical application to correctly detach your easily-tested logic from the presentation code, which manages user interaction.

This article is an excerpt taken from the book Hands-On GUI Application Development in Go. This book covers the benefits and complexities of building native graphical applications, the procedure for building platform and developing graphical Windows applications using Walk. 

In this article, we will learn certain aspects of best practices that make it easier to maintain and grow GUI-based applications. This article covers separation of concerns, test-driving UI development and much more.

By separating the concerns of an application, it is easier to test subcomponents and check the validity of our software without even needing to run the application. In doing so, we create more robust applications that can adapt to changes in requirements or technology over time. For example, the graphical toolkit that you choose for your application should not be incorporated into, or impact the design of, your business logic.

Suggested application structure

As you plan the development of your application, consider how the core concerns could be separated to maintain flexibility. The following suggested structure should provide some inspiration:

project/ The root of the project structure. This package should define the interfaces and utility functions used by the rest of the project. These files should not depend on any sub-packages.
project/logic/ This package will contain most of your application logic. Careful consideration should be given to which functions and types are exposed, as they will form the API that the rest of your application will depend upon. There may be multiple packages that contain application logic as you separate the application’s concerns. An alternative, domain-specific term may be preferred to logic.
project/storage/ Most applications will rely upon a data source of some kind. This package will define one or many possible data sources. They will conform to an interface in the top-level project so that data access can be passed between packages of the project.
project/gui/ This package is the only place where your graphical toolkit should be imported. It is responsible for loading your application GUI and responding to user events. It will probably access data provided by a storage package set from the application runner.
project/cmd/appname/ The Go convention for application binaries is that they reside within a cmd/appname sub-package. The actual package for this directory will be main, and it will contain, minimal code that is required to load and run the main application defined within the other packages. It will probably initialize a storage system, load the application logic, and instruct the graphical interface to load.

When writing tests in each of these packages, they will focus on the functionality of the current package. The logic package should have very high unit-test coverage, whereas the storage package may rely more on integration testing. The gui package, which is often considered the hardest to test, could directly import the logic package in its tests, but should probably not include the main storage package to validate its functionality.

Following a sensible structure will aid significantly in making your application testable, as many developers are probably already aware. It is often much harder, however, to test the graphical portions of an application. Designing your application to be unit-testable from the beginning will often result in a code base that is better organized and will naturally lead to code that is easier to understand and change. Let’s take a look at what Test-driven Development (TDD) can teach us about building graphical interfaces.

Test-driving UI development

The effort required to automatically test user interfaces or frontend software is often debated as being far too expensive for the value it returns in avoiding future bugs. However, this is largely rooted in the toolkits being utilized or even the presentation technologies chosen. Without full support for testing in the development tools or graphical APIs, it can indeed be difficult to create simple unit tests without a huge effort. As seen frequently in web-based environments (and some native test frameworks), the only remaining possibility is to run the application and execute test scripts that will perform the validation. They will typically control the user input, simulating mouse actions and keyboard taps, and monitor the resulting behavior of the application under test. If, however, your application and GUI toolkit are architected with testing in mind (for example, using separation of concerns), automated tests should be possible with far less overhead.

Designed to be testable

When setting out the components within a project’s UI code (as illustrated in the gui sub-package), care should be taken to define types and classes that have a single responsibility and a clear API. Doing so will make it easier to load and test individual components with the standard Go testing tools. If smaller components can be tested, we can avoid launching the whole application and the required test runners, therefore making the testing process much faster. When a test suite runs quickly, it can be run more frequently and extended more easily, leading to higher test coverage and greater confidence in the software quality.

For a practical example, let’s look at the GoMail compose dialog and its Send button. Clearly, the dialog box should perform all sorts of validation before sending, and if they pass then send the email. Validation can easily be tested with normal unit tests, but verifying that the send button correctly sends a new email will require the user interface to be tested. In the following example, we will load the compose window, enter some data, and simulate the Send button being pressed. By using a test email server, as used through each of the GoMail examples, we can check that the email has been sent by the user interface without needing to communicate with a real email server.

Example application test

As the tests are in the same package, we can test internal function definitions rather than relying on exported APIs—this is common with UI code as long as the application is not large enough to warrant separate packages or libraries. We start by adding the test imports; testing is required for go test code and github.com/stretchr/testify/assert provides helpful assertion functionality. We also import the client email library created for our GoMail examples and finally the Fyne test package, fyne.io/fyne/test:

package main
import ( 
  "testing"   

"fyne.io/fyne/test"   

"github.com/PacktPublishing/Hands-On-GUI-Application-Development-in-Go/client"
   "github.com/stretchr/testify/assert"
)

Now we can add a test method using the recommended naming pattern of Test<type>_<function>(); normally, the function would be a function name, but here we refer to the button title or its action. In the first part of the function, we set up the compose window for testing by calling newCompose() and passing it a test application (returned from test.NewApp()). We then prepare the state for our test—we record the size of the server outbox and set up an OnClosed handler that will report when the window is closed. Finally, we simulate typing an email address into the compose.to field using test.Type():

func TestCompose_Send(t *testing.T) {   
server := client.NewTestServer()   
compose := newCompose(test.NewApp(), server)   
ui := compose.loadUI()   

pending := len(server.Outbox)   
closed := false   
ui.SetOnClosed(func() {     
 closed = true  
 })   
address := "[email protected]"   test.Type(compose.to, address)   
...
}

Once the setup code is complete, we can implement the main test. This starts by using test.Tap() to tap the compose.send button, which should cause an email to be sent. We first verify that the window was closed after the email send completes (the OnClosed handler we added records this). Then we check that there is one more email in the server.Outbox than before.

If these tests pass, we will move to the final check. The email that was sent is extracted from the outbox so we can examine its content. With one final assertion, we verify that the email address matched what we typed into the To input box:

func TestCompose_Send(t *testing.T) {  
 ...   
test.Tap(compose.send)   
assert.True(t, closed)   

assert.Equal(t, pending + 1, len(server.Outbox))   
email := server.Outbox[len(server.Outbox)-1]   assert.Equal(t, address, email.ToEmailString())
}

Running the preceding test will load the user interface in memory, execute the setup code, and run the tests, and then exit with the results. We run the following test with -v to see each test that is run rather than just a summary. You will notice that testing in this way takes very little time (go test reports 0.00 seconds for the test and 0.004 seconds in total); therefore, many more tests could be run on a regular basis to verify the application’s behavior:

Running the user interface test took very little time

When running the tests, you may notice that this test does not cause any window to be displayed on your computer screen. This is a design feature of many test frameworks for GUI toolkits – it is much faster to run the application without displaying it for test purposes. This is often called headless mode and is very useful when running automated tests as part of a continuous integration process.

Continuous integration for GUIs

Continuous integration (the regular merging of a team’s work-in-progress code to be automatically tested) has become commonplace in software development teams. Adding this process to your team workflow is shown to highlight issues earlier in the development process, which leads to fixing issues faster and, ultimately, better-quality software. A critical part of this is the automation of tests that exercise the whole of the source code, which includes the graphical user interface.

Approaches to GUI test automation

It is important to organize your code into logical components for development and testing. Using the framework test features (or external support libraries) smaller components can more easily be verified through simple tests. The Go language’s built-in support for testing has meant that test coverage is improving; in fact, the popular Go library list, on Awesome, asks that libraries have a test coverage of at least 80%!

If your chosen framework does not provide the necessary support, it is still possible to automate functional testing. The technique involves running the application from a test script that then performs simulated user actions on the host computer. This is not ideal as it requires the application to be visible on the screen and for the test script to take control of the keyboard and mouse – but it is better than having no GUI testing in place. To work around this inconvenience, it is possible to load a virtual frame buffer (an off-screen display area) in which to run the application. This technique basically creates an invisible screen to which the application can draw.

Avoiding external dependencies

One thing to be aware of when testing an application, or portions of it, is that there may be external systems involved. A file browser may rely on network connections for some of its work, or an instant messenger app is going to need a server to handle sending and receiving messages. If your code has been organized carefully to separate its concerns, you will already have used interfaces to define the interactions between different components. If this approach is taken, we can use dependency injection to provide alternative implementations for areas of an application that should not be included in automated testing.

When code is properly decoupled from the components that it relies on, it’s possible to load different versions of an application for testing. In this manner, we can avoid relying on any external systems or causing permanent changes to a data store. Let’s look at a trivial example, a Storage interface is defined that will be used to read and write files from a disk:

type Storage interface {
   Read(name string) string
   Write(name, content string)
}

There is an application runner that invokes permanent storage and uses it to write and then read a file:

func runApp(storage Storage) {  
 log.Println("Writing README.txt")   storage.Write("README.txt", "overwrite")  
 log.Println("Reading README.txt")   log.Println(storage.Read("README.txt"))
}
func main() {   
runApp(NewPermanentStorage())
}

Clearly, this application will cause whatever was in an existing README.txt file to be overwritten with the contents of overwrite. If we assume, for example, that this is the desired behavior, we probably don’t want this external system (the disk) to be affected by our tests. Because we have designed the storage to conform to an interface, our test code can include a different storage system that we can use in tests, as follows:

type testStorage struct {  
 items map[string]string
}
func (t *testStorage) Read(name string) string {   return t.items[name]
}
func (t *testStorage) Write(name, content string) {   t.items[name] = content
}
func newTestStorage() Storage {  
store := &testStorage{}   
store.items = make(map[string]string)  
 return store
}

Following this addition, we can test our application’s runApp function without the risk of overwriting real files:

import (  
 "testing"  

 "github.com/stretchr/testify/assert"
)
func TestMain_RunApp(t *testing.T) {   
testStore := newTestStorage()   
runApp(testStore)   

newFile := testStore.Read("README.txt")   assert.Equal(t, "overwrite", newFile)
}

When running this test, you will see that we get the expected result, and should also notice that no real files have changed.

See that our TestMain_RunApp completed successfully without writing to our disk

In this article, we explored some of the tips and techniques for managing a GUI-based application written with Go. We have learned about the separation of concerns, test-driving UI development and much more. To know more about managing specific platforms with Go, check out the book Hands-On GUI Application Development in Go.

Read Next

GitHub releases Vulcanizer, a new Golang Library for operating Elasticsearch

State of Go February 2019 – Golang developments report for this month released

The Golang team has started working on Go 2 proposals