Programming Testing: Seams: Difference between revisions

From wikinotes
 
(19 intermediate revisions by the same user not shown)
Line 1: Line 1:
Seams are places where you can alter the behaviour of a program when it is under test.<br>
Seams are places where you can alter the behaviour of a program when it is under test.<br>
The available seams depend on the programming language in use.<br>
When designing a program for testing, you should provide seams to allow it to be tested.
When designing a program for testing, you should provide seams to allow it to be tested.


= PreProcessor Seams =
Seams allow you to:
* substitute a file/method/object for another
* override a method/object
* type params, package vars with interfaces and pass/swap with a fake
 
= Application Design =
<blockquote>
When writing your application, think about areas that:
 
* have side effects (ex. filesystem operations)
* need to expose testing-points (ex. logged messages)
* handle behaviours (ex. renderers)
 
Consider defining interfaces for these, and variations so they can be tested/isolated.
</blockquote><!-- Seam Design -->
 
= Seam Types =
<blockquote>
== PreProcessor Seams ==
<blockquote>
<blockquote>
{{ TODO |
untested }}


In C/C++ and other languages with macros,<br>
In C/C++ and other languages with macros,<br>
you can use an <code>#ifdef</code> to inject or replace test methods.
you can use an <code>#ifdef</code> to inject or replace test methods.
{{ expand
| c example
|
{{ TODO |
untested }}


In the real file, we include <code>testmacros.h</code>,<br>
In the real file, we include <code>testmacros.h</code>,<br>
Line 51: Line 73:
#endif
#endif
</syntaxhighlight>
</syntaxhighlight>
}}


</blockquote><!-- PreProcessor Seams -->
</blockquote><!-- PreProcessor Seams -->


= Linker/Import Seams =
== Linker/Import Seams ==
<blockquote>
<blockquote>
You can alter the path code is sourced from to substitute in entirely new files.
You can alter the path code is sourced from to substitute in entirely new files.
Line 65: Line 88:
</blockquote><!-- Linker Seams -->
</blockquote><!-- Linker Seams -->


= Package Seams =
== Package/Module Seams ==
<blockquote>
<blockquote>
Reassign a package variable for a single testrun.
{{ expand
| go example
|
This package defines OsCreate, a function that is used throghout the codebase.
<syntaxhighlight lang="go">
// internal/fs/fs.go
package fs
import "os"
var OsCreate = os.Create // exported function, we can override
</syntaxhighlight>
We call fs.OsCreate here
<syntaxhighlight lang="go">
// foo.go
package main
import "foo.com/x/foo/internal/fs"
func Foo() {
    _, err := fs.OsCreate("/var/tmp/foo.txt")
    if err != nil {
        panic(err)
    }
}
</syntaxhighlight>
In our testfile, we swap OsCreate with one that returns an error
<syntaxhighlight lang="go">
// foo_test.go
package main
import (
    "errors"
    "foo.com/x/foo/internal/fs"
)
func TestFoo(t *testing.T) {
    var ExpectedError = errors.New("Expected")
    fs.OsCreate = func(path string) (*os.File, error) {
        return nil, ExpectedError
    }


    foo.Foo()  // <-- when foo calls 'fs.OsCreate', it will return expected error
}
</syntaxhighlight>
}}
</blockquote><!-- Package Seams -->
</blockquote><!-- Package Seams -->


= Object Seams =
== Object Seams ==
<blockquote>
<blockquote>
=== Dependency Injection ===
<blockquote>
Don't get information, and use it in one method.<br>
Get info, then Pass information into method in separate steps.


{{ expand
| go example
|
Bad
<syntaxhighlight lang="go">
func getUser() User {
    return User{First: "luke", Last: "skywalker"}
}
func FullName() string {
    user := getUser()
    return fmt.Sprint(user.First, user.Last)
}
func Main() {
    user = getUser()
    FullName(user.First, user.Last)
}
</syntaxhighlight>
Good
<syntaxhighlight lang="go">
func getUser() User {
    return User{First: "luke", Last: "skywalker"}
}
func FullName(first, last string) string {
    return fmt.Sprint(first, last)
}
func Main() {
    user = getUser()
    FullName(user.First, user.Last)
}
</syntaxhighlight>
}}
</blockquote><!-- Dependency Injection -->
</blockquote><!-- Object Seams -->
</blockquote><!-- Object Seams -->


= Interface Seams =
== Interface Seams ==
<blockquote>
=== Inversion of Control ===
<blockquote>
Params or Attributes typed as interfaces rather than concretions lets you sub in a fake.<br>
You can also do this by passing lambdas.
 
{{ expand
| go example
|
 
Define an interface
<syntaxhighlight lang="go">
// internal/interfaces/user.go
type UserInterface interface {
    Rename(n string) error
}
</syntaxhighlight>
 
Real code implements interface
<syntaxhighlight lang="go">
// user.go
 
type User struct {}
 
func (this *User) Rename(n string) error {
    // ...
}
</syntaxhighlight>
 
Fake also impelements interface
<syntaxhighlight lang="go">
type FakeUser struct {
    RenameError error
}
 
func (this *FakeUser) Rename(n string) error {
    return this.RenameError
}
</syntaxhighlight>
 
Foo accepts UserInterface, rather than concrete User.
<syntaxhighlight lang="go">
// foo.go
 
func Foo(user UserInterface) error {
    return user.Rename("vader")
}
</syntaxhighlight>
 
Within test we'll pass the fake, which we can manipulate to return an error
<syntaxhighlight lang="go">
// foo_test.go
 
var ExpectedError = errors.New("Expected")
 
func TestFoo(t *testing.T) {
    user := FakeUser{RenameError: ExpectedError}
    foo.Foo(user)
}
</syntaxhighlight>
 
}}
</blockquote><!-- Inversion of Control -->
 
=== System Interface ===
<blockquote>
<blockquote>
Rather than creating an interface for every object-type,<br>
it might make sense to define an entire system (ex. all db calls).<br>
Then you could combine this with a '''Package Seam'''.


For example, [[golang afero]] defines an interface for all os/io operations.<br>
One concretion performs real operations, another records them in-memory without changing filesystem.
</blockquote><!-- System Interface -->
</blockquote><!-- Interface Seams -->
</blockquote><!-- Interface Seams -->
</blockquote><!-- Seam Types -->

Latest revision as of 15:20, 23 July 2022

Seams are places where you can alter the behaviour of a program when it is under test.
When designing a program for testing, you should provide seams to allow it to be tested.

Seams allow you to:

  • substitute a file/method/object for another
  • override a method/object
  • type params, package vars with interfaces and pass/swap with a fake

Application Design

When writing your application, think about areas that:

  • have side effects (ex. filesystem operations)
  • need to expose testing-points (ex. logged messages)
  • handle behaviours (ex. renderers)

Consider defining interfaces for these, and variations so they can be tested/isolated.

Seam Types

PreProcessor Seams

In C/C++ and other languages with macros,
you can use an #ifdef to inject or replace test methods.

c example


TODO:

untested

In the real file, we include testmacros.h,
which in our tests will define functions locally.

// main.c

#include <stdio.h>
#include "libusers.h"
#include "testmacros.h"  // <-- when testing, define 'create_db_user' locally

int main(int argc, char *argv[]) {
    int id = 100;
    create_db_user("foo@example.com", "foo");  // <-- this will use our macro defined func
}

In our macro, we define a function that fakes creating a user in the database.

// testmacros.h

#ifdef TESTING

struct User {
    int  id;
    char *email;
    char *name;
}

User users[5] = {};

#define create_db_user(email, name) \
    { \
        struct User user; \
        user.id = 123; \
        user.email = email; \
        user.name = name; \
        users[0] = user; \
    }

#endif

Linker/Import Seams

You can alter the path code is sourced from to substitute in entirely new files.

CLASSPATH=test/foo:${CLASSPATH}    # java
GOPATH=test/foo:${GOPATH}          # go
PYTHONPATH=test/foo:${PYTHONPATH}  # python

Package/Module Seams

Reassign a package variable for a single testrun.

go example


This package defines OsCreate, a function that is used throghout the codebase.

// internal/fs/fs.go
package fs

import "os"

var OsCreate = os.Create // exported function, we can override

We call fs.OsCreate here

// foo.go
package main

import "foo.com/x/foo/internal/fs"

func Foo() {
    _, err := fs.OsCreate("/var/tmp/foo.txt")
    if err != nil {
        panic(err)
    }
}

In our testfile, we swap OsCreate with one that returns an error

// foo_test.go
package main

import (
    "errors"
    "foo.com/x/foo/internal/fs"
)

func TestFoo(t *testing.T) {
    var ExpectedError = errors.New("Expected")
    fs.OsCreate = func(path string) (*os.File, error) {
        return nil, ExpectedError
    }

    foo.Foo()  // <-- when foo calls 'fs.OsCreate', it will return expected error
}

Object Seams

Dependency Injection

Don't get information, and use it in one method.
Get info, then Pass information into method in separate steps.

go example


Bad

func getUser() User {
    return User{First: "luke", Last: "skywalker"}
}

func FullName() string {
    user := getUser()
    return fmt.Sprint(user.First, user.Last)
}

func Main() {
    user = getUser()
    FullName(user.First, user.Last)
}

Good

func getUser() User {
    return User{First: "luke", Last: "skywalker"}
}

func FullName(first, last string) string {
    return fmt.Sprint(first, last)
}

func Main() {
    user = getUser()
    FullName(user.First, user.Last)
}

Interface Seams

Inversion of Control

Params or Attributes typed as interfaces rather than concretions lets you sub in a fake.
You can also do this by passing lambdas.

go example


Define an interface

// internal/interfaces/user.go
type UserInterface interface {
    Rename(n string) error
}

Real code implements interface

// user.go

type User struct {}

func (this *User) Rename(n string) error {
    // ...
}

Fake also impelements interface

type FakeUser struct {
    RenameError error
}

func (this *FakeUser) Rename(n string) error {
    return this.RenameError
}

Foo accepts UserInterface, rather than concrete User.

// foo.go

func Foo(user UserInterface) error {
    return user.Rename("vader")
}

Within test we'll pass the fake, which we can manipulate to return an error

// foo_test.go

var ExpectedError = errors.New("Expected")

func TestFoo(t *testing.T) {
    user := FakeUser{RenameError: ExpectedError}
    foo.Foo(user)
}


System Interface

Rather than creating an interface for every object-type,
it might make sense to define an entire system (ex. all db calls).
Then you could combine this with a Package Seam.

For example, golang afero defines an interface for all os/io operations.
One concretion performs real operations, another records them in-memory without changing filesystem.