Testing components¶
Testing is an essential part of the software development life cycle. This page covers everything you need to know about testing components.
One of the core benefits of using components is that each component isolates its internal logic behind its interface. Focus on asserting that each implementation behaves correctly.
To recap from the previous page, a component was created that compresses the payload before sending it to the Datadog backend. The component has two separate implementations.
This is the component's interface:
type Component interface {
// Compress compresses the input data.
Compress([]byte) ([]byte, error)
// Decompress decompresses the input data.
Decompress([]byte) ([]byte, error)
}
Ensure the Compress
and Decompress
functions behave correctly.
Writing tests for a component implementation follows the same rules as any other test in a Go project. See the testing package documentation for more information.
For this example, write a test file for the zstd
implementation. Create a new file named component_test.go
in the impl-zstd folder
. Inside the test file, initialize the component's dependencies, create a new component instance, and test the behavior.
Initialize the component's dependencies¶
All components expect a Requires
struct with all the necessary dependencies. To ensure a component instance can be created, create a requires
instance.
The Requires
struct declares a dependency on the config component and the log component. The following code snippet shows how to create the Require
struct:
package implzstd
import (
"testing"
configmock "github.com/DataDog/datadog-agent/comp/core/config/mock"
logmock "github.com/DataDog/datadog-agent/comp/core/log/mock"
)
func TestCompress(t *testing.T) {
logComponent := configmock.New(t)
configComponent := logmock.New(t)
requires := Requires{
Conf: configComponent,
Log: logComponent,
}
// [...]
}
To create the log and config component, use their respective mocks. The mock package was mentioned previously in the Creating a Component page.
Testing the component's interface¶
Now that the Require
struct is created, an instance of the component can be created and its functionality tested:
package implzstd
import (
"testing"
configmock "github.com/DataDog/datadog-agent/comp/core/config/mock"
logmock "github.com/DataDog/datadog-agent/comp/core/log/mock"
)
func TestCompress(t *testing.T) {
logComponent := configmock.New(t)
configComponent := logmock.New(t)
requires := Requires{
Conf: configComponent,
Log: logComponent,
}
provides := NewComponent(requires)
component := provides.Comp
result, err := component.Compress([]byte("Hello World"))
assert.Nil(t, err)
assert.Equal(t, ..., result)
}
Testing lifecycle hooks¶
Sometimes a component uses Fx lifecycle to add hooks. It is a good practice to test the hooks as well.
For this example, imagine a component wants to add some hooks into the app lifecycle. Some code is omitted for simplicity:
package impl
import (
"context"
somecomponent "github.com/DataDog/datadog-agent/comp/somecomponent/def"
compdef "github.com/DataDog/datadog-agent/comp/def"
)
type Requires struct {
Lc compdef.Lifecycle
}
type Provides struct {
Comp somecomponent.Component
}
type component struct {
started bool
stopped bool
}
func (c *component) start() error {
// [...]
c.started = true
return nil
}
func (h *healthprobe) stop() error {
// [...]
c.stopped = true
c.started = false
return nil
}
// NewComponent creates a new healthprobe component
func NewComponent(reqs Requires) (Provides, error) {
provides := Provides{}
comp := &component{}
reqs.Lc.Append(compdef.Hook{
OnStart: func(ctx context.Context) error {
return comp.start()
},
OnStop: func(ctx context.Context) error {
return comp.stop()
},
})
provides.Comp = comp
return provides, nil
}
The goal is to test that the component updates the started
and stopped
fields.
To accomplish this, create a new lifecycle instance, create a Require
struct instance, initialize the component, and validate that calling Start
on the lifecycle instance calls the component hook and executes the logic.
To create a lifecycle instance, use the helper function compdef.NewTestLifecycle(t *testing.T)
. The function returns a lifecycle wrapper that can be used to populate the Requires
struct. The Start
and Stop
functions can also be called.
Info
You can see the NewTestLifecycle
function here
package impl
import (
"context"
"testing"
compdef "github.com/DataDog/datadog-agent/comp/def"
"github.com/stretchr/testify/assert"
)
func TestStartHook(t *testing.T) {
lc := compdef.NewTestLifecycle(t)
requires := Requires{
Lc: lc,
}
provides, err := NewComponent(requires)
assert.NoError(t, err)
assert.NotNil(t, provides.Comp)
internalComponent := provides.Comp.(*component)
ctx := context.Background()
lc.AssertHooksNumber(1)
assert.NoError(t, lc.Start(ctx))
assert.True(t, internalComponent.started)
}
For this example, a type cast operation had to be performed because the started
field is private. Depending on the component, this may not be necessary.