Skip to main content

Set Up Service Subpackage and Interface

Since we're going to be creating data service objects, we'll be following the guidelines in Structure - Data/Utility High Level Structure.

For the purposes of this walk-through, that means creating a subpackage in the services package called pet and a corresponding interface file called pet.go, like this:

mymove/
├── pkg/
│ ├── services/
│ │ ├── ...
│ │ ├── pet/ <- our new subpackage
│ │ ├── ...
│ │ ├── pet.go <- our new interface in the services package
│ │ ├── ...

Service Interface

The services/pet.go file will define the interface for our service objects. As noted in the structure page, this file will only have the interface, while the implementation logic will live in the subpackage. This means that the interface's functions that we define here will have to match the receiver functions for our service object structs we create later in the implementation files.

Interfaces vs Structs

If you are new to Go and are still a little wobbly on the concept of "interfaces" vs "structs" remember:

  • Interface types define functions. They are concerned with verbs.
  • Struct types define objects. They are concerned with nouns.

Let's start off by defining the bare interfaces for a PetCreator and a PetUpdater:

pkg/services/pet.go
package services

// PetCreator Interface for the service object that creates a pet
type PetCreator interface {
}

// PetUpdater Interface for the service object that updates a pet
type PetUpdater interface {
}

Following our naming conventions stated in Structure - Service Object Naming, we'll define the interface functions like so:

pkg/services/pet.go
package services

// PetCreator Interface for the service object that creates a pet
type PetCreator interface {
CreatePet() ()
}

// PetUpdater Interface for the service object that updates a pet
type PetUpdater interface {
UpdatePet() ()
}

Now we'll need to think about what we'll need to take in as parameters and what we'll want to return.

Parameters

Service objects should be reusable and modular, so keep this in mind while defining your parameters. To start with, they should be the bare minimum needed for someone to call this function. Use your best judgment.

info

You will always need to pass in the AppContext as the first argument. This is standard in our codebase. Read more about AppContext and how to use it.

Often, the particular model type you are dealing with is passed in as input as well. For our example, we'll have a service object to create a pet and one to update a pet, so we'll need to take in a models.Pet type. For updates, we'll also need to take in an E-tag per ADR 0042 Optimistic Locking.

So now our interfaces look like this:

pkg/services/pet.go
package services

import (
"github.com/transcom/mymove/pkg/appcontext"
"github.com/transcom/mymove/pkg/models"
)

// PetCreator Interface for the service object that creates a pet
type PetCreator interface {
CreatePet(appCtx appcontext.AppContext, pet models.Pet) ()
}

// PetUpdater Interface for the service object that updates a pet
type PetUpdater interface {
UpdatePet(appCtx appcontext.AppContext, pet models.Pet, eTag string) ()
}

Return values

Service objects should return as many return values as appropriate, and this will always include possible errors. A common convention is to return the pointer of the subject model type and a possible error. So taking that into account, our interfaces now look like this:

pkg/services/pet.go
package services

import (
"github.com/transcom/mymove/pkg/appcontext"
"github.com/transcom/mymove/pkg/models"
)

// PetCreator Interface for the service object that creates a pet
type PetCreator interface {
CreatePet(appCtx appcontext.AppContext, pet models.Pet) (*models.Pet, error)
}

// PetUpdater Interface for the service object that updates a pet
type PetUpdater interface {
UpdatePet(appCtx appcontext.AppContext, pet models.Pet, eTag string) (*models.Pet, error)
}

Mocks

The final piece we need to define at the interface level is the mocks. When our service objects are dependencies elsewhere, we want to be able to mock them out for those tests, letting them focus on what they're testing rather than having to worry about what our service object does. We use mockery to generate our mocks. To set up our service objects to be able to be mocked, we'll need to add tags that go:generate can read, like so:

pkg/services/pet.go
package services

import (
"github.com/transcom/mymove/pkg/appcontext"
"github.com/transcom/mymove/pkg/models"
)

// PetCreator Interface for the service object that creates a pet
//go:generate mockery --name PetCreator --disable-version-string
type PetCreator interface {
CreatePet(appCtx appcontext.AppContext, pet models.Pet) (*models.Pet, error)
}

// PetUpdater Interface for the service object that updates a pet
//go:generate mockery --name PetUpdater --disable-version-string
type PetUpdater interface {
UpdatePet(appCtx appcontext.AppContext, pet models.Pet, eTag string) (*models.Pet, error)
}

Note that the name we pass in the --name argument matches the name of our interface type. That's what will get used by any test that wants to mock out our service.

Now you could generate the mocks using make mocks_generate and you should see that new mock files are created for the new service objects.

Service Test File

Now we'll add the boilerplate testing suite setup mentioned in Service Object Subpackage Structure. We'll add a file called pet_service_test.go in the pet subpackage like so:

mymove/
├── pkg/
│ ├── services/
│ │ ├── ...
│ │ ├── pet/
│ │ │ ├── pet_service_test.go <- boilerplate testing suite setup
│ │ ├── ...

Here is what the file will contain:

package pet

import (
"testing"

"github.com/stretchr/testify/suite"

"github.com/transcom/mymove/pkg/testingsuite"
)

type PetSuite struct {
testingsuite.PopTestSuite
}

func TestPetServiceSuite(t *testing.T) {
ts := &PetSuite{
PopTestSuite: testingsuite.NewPopTestSuite(testingsuite.CurrentPackage(), testingsuite.WithPerTestTransaction()),
}

suite.Run(t, ts)

ts.PopTestSuite.TearDown()
}

This file sets up our pet subpackage to enable running tests in transactions.

That's it for this step, we'll add more files to the subpackage as we go along the process and fill in the interface when we start setting up our service objects.