
Test and run a Worker
- Build the application
- Test and run a Worker
- Run and observe retries
Now that the Workflow and Activities are in place, you'll configure a Worker to host them and write tests to verify they behave as expected. The Worker polls a Task Queue and runs your code when work arrives.
Configure and run a Worker
Create a new directory called worker which will hold the Worker program:
mkdir worker
Now create the file main.go in that directory:
touch worker/main.go
Then open worker/main.go in your editor and add the following code to define the Worker program:
package main
import (
"log"
"net/http"
"temporal-ip-geolocation/iplocate"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/worker"
)
func main() {
// Create the Temporal client
c, err := client.Dial(client.Options{})
if err != nil {
log.Fatalln("Unable to create Temporal client", err)
}
defer c.Close()
// Create the Temporal worker
w := worker.New(c, iplocate.TaskQueueName, worker.Options{})
// inject HTTP client into the Activities Struct
activities := &iplocate.IPActivities{
HTTPClient: http.DefaultClient,
}
// Register Workflow and Activities
w.RegisterWorkflow(iplocate.GetAddressFromIP)
w.RegisterActivity(activities)
// Start the Worker
err = w.Run(worker.InterruptCh())
if err != nil {
log.Fatalln("Unable to start Temporal worker", err)
}
}
The code imports the iplocate package, which includes your Workflow and Activity Definitions. It defines a main function that creates and runs a Worker.
You first create a client, and then you create a Worker that uses the client, along with the Task Queue it should listen on. By default, the client connects to the Temporal Cluster running at localhost on port 7233, and connects to the default namespace. You can change this by setting values in client.Options.
Then you register your Workflow and Activities with the Worker. Since you defined your Activities as a struct, you use that instead of referencing your Activities directly. This is also where you inject the HTTP client so your Activities can access it.
In this case your Worker will run your Workflow and your two Activities, but there are cases where you could configure one Worker to run Activities, and another Worker to run the Workflows.
Now you'll start the Worker. Be sure you have started the local Temporal Service and execute the following command to start your Worker:
go run worker/main.go
The Worker runs and you see the following output:
2024/12/16 14:32:44 INFO No logger configured for temporal client. Created default one.
2024/12/16 14:32:44 INFO Started Worker Namespace default TaskQueue ip-address-go WorkerID 31530@temporal-2.local@
Your Worker is running and is polling the Temporal Service for Workflows to run, but before you start your Workflow, you'll write tests to prove it works as expected.
Write a Workflow test
The Temporal Go SDK includes functions that help you test your Workflow executions. Let's add a basic unit test to the application to make sure the Workflow works as expected.
You'll use the testify package to build your test cases and mock the Activity so you can test the Workflow in isolation.
Add the testify/mock and testify/assert packages to your project by running the following commands in your terminal:
go get github.com/stretchr/testify/mock
go get github.com/stretchr/testify/assert
go mod tidy
Now create a file to hold the test for your Workflow. Create the file workflows_test.go in your project root:
touch workflows_test.go
Add the following code to set up the testing environment:
package iplocate_test
import (
"testing"
"temporal-ip-geolocation/iplocate"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.temporal.io/sdk/testsuite"
)
Add the following code to test the Workflow execution:
func Test_Workflow(t *testing.T) {
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestWorkflowEnvironment()
activities := &iplocate.IPActivities{}
// Mock activity implementation
env.OnActivity(activities.GetIP, mock.Anything).Return("1.1.1.1", nil)
env.OnActivity(activities.GetLocationInfo, mock.Anything, "1.1.1.1").Return("Planet Earth", nil)
env.ExecuteWorkflow(iplocate.GetAddressFromIP, "Temporal")
var result string
assert.NoError(t, env.GetWorkflowResult(&result))
assert.Equal(t, "Hello, Temporal. Your IP is 1.1.1.1 and your location is Planet Earth", result)
}
This test creates a test execution environment to run the Workflow.
Instead of using your actual Activities, you replace the Activities GetIP and GetLocationInfo with mocks that return hard-coded values. This way you're testing the Workflow's logic independently of the Activities. Since each Activity accepts a context as its first argument, you use mock.Anything as a substitute for the context since it isn't required for this test.
The test then executes the Workflow in the test environment and checks for a successful execution. Finally, the test ensures the Workflow's return value matches the expected value.
Ensure you've saved all your files and execute your tests with the following command:
go test
The test environment starts, spins up a Worker, and executes the Workflow in the test environment. At the end, you'll see that your test passes:
2024/12/17 10:25:15 DEBUG handleActivityResult: *workflowservice.RespondActivityTaskCompletedRequest. ActivityID 1 ActivityType GetIP
2024/12/17 10:25:15 DEBUG handleActivityResult: *workflowservice.RespondActivityTaskCompletedRequest. ActivityID 2 ActivityType GetLocationInfo
PASS
ok iplocate 0.397s
Write Activity tests
With a Workflow test in place, you can write unit tests for the Activities.
Both of your Activities make external calls to services that will change their results based on who runs them. It will be challenging to test these Activities reliably. For example, the IP address may vary based on your machine's location.
To ensure you can test the Activities in isolation, you'll stub out the HTTP calls.
Create the file activities_test.go:
touch activities_test.go
Add the following code to import the testing libraries and Activities you'll use, and then define types for your mock HTTP client and mock response:
package iplocate_test
import (
"io"
"net/http"
"strings"
"testing"
"temporal-ip-geolocation/iplocate"
"github.com/stretchr/testify/assert"
"go.temporal.io/sdk/testsuite"
)
type MockHTTPClient struct {
Response *http.Response
Err error
}
func (m *MockHTTPClient) Get(url string) (*http.Response, error) {
return m.Response, m.Err
}
To ensure you don't make real HTTP requests, you define a mock HTTP client struct with a Get function. When you write your tests you'll inject this mocked client into the Activity so you can control the response.
Next, write the test for the GetIP Activity, using your mocked HTTP client to stub out actual HTTP calls so your tests are consistent. Notice that the mock response adds the newline character so it replicates the actual response:
// TestGetIP tests the GetIP activity with a mock server.
func TestGetIP(t *testing.T) {
// set up test environment
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestActivityEnvironment()
// Create a mock response that returns the fake IP address
mockResponse := &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader("127.0.0.1\n")),
}
// load Activities and inject mock response
ipActivities := &iplocate.IPActivities{
HTTPClient: &MockHTTPClient{Response: mockResponse},
}
env.RegisterActivity(ipActivities)
// Call the GetIP function
val, err := env.ExecuteActivity(ipActivities.GetIP)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
// get the Activity result
var ip string
val.Get(&ip)
// Validate the returned IP
expectedIP := "127.0.0.1"
assert.Equal(t, ip, expectedIP)
}
Like in your Worker, you inject the HTTP client into the Activities struct and then register the Activities with the test environment.
To test the Activity itself, you use the test environment to execute the Activity rather than directly calling the GetIP function. You get the result from the Activity Execution and then ensure it matches the value you expect.
To test the GetLocationInfo Activity, you use a similar approach. Add the following code to the activities_test.go file:
// TestGetLocationInfo tests the GetLocationInfo activity with a mock server.
func TestGetLocationInfo(t *testing.T) {
// set up test environment
testSuite := &testsuite.WorkflowTestSuite{}
env := testSuite.NewTestActivityEnvironment()
mockResponse := &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`{
"city": "San Francisco",
"regionName": "California",
"country": "United States"
}`)),
}
ipActivities := &iplocate.IPActivities{
HTTPClient: &MockHTTPClient{Response: mockResponse},
}
env.RegisterActivity(ipActivities)
ip := "127.0.0.1"
val, err := env.ExecuteActivity(ipActivities.GetLocationInfo, ip)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
var location string
val.Get(&location)
expectedLocation := "San Francisco, California, United States"
assert.Equal(t, location, expectedLocation)
}
This test looks similar to the previous one; you mock out the HTTP client and ensure it returns the expected data, and then you execute the Activity in the test environment. Then you retrieve the value and ensure it's what you expect.
Run the tests again to see them pass. Use the -v option to see verbose output so you can see that all the tests ran:
go test -v
=== RUN TestGetIP
--- PASS: TestGetIP (0.03s)
=== RUN TestGetLocationInfo
--- PASS: TestGetLocationInfo (0.00s)
=== RUN Test_Workflow
2024/12/17 10:31:07 DEBUG handleActivityResult: *workflowservice.RespondActivityTaskCompletedRequest. ActivityID 1 ActivityType GetIP
2024/12/17 10:31:07 DEBUG handleActivityResult: *workflowservice.RespondActivityTaskCompletedRequest. ActivityID 2 ActivityType GetLocationInfo
--- PASS: Test_Workflow (0.00s)
PASS
ok iplocate 0.395s
Now that you have your tests passing, it's time to start a Workflow Execution and observe how Temporal handles failures.
Get notified when we launch new educational content
New courses, tutorials, and learning resources - straight to your inbox.