Increase coverage (#42)

* Introduce mocking framework and increase coverage (#22)

This change introduces two common golang patterns:

- types: this will allow fine-tuned control over imported
types by defining where they will be used and how

- mock: this allows the generation of mock constructors,
which allows for testing any individual path in a method by
"injecting" a mock method which matches the expected type

This change also increases test coverage to 100%

Co-authored-by: Yoshiyuki Mineo <Yoshiyuki.Mineo@jp.sony.com>

* gofmt

---------

Co-authored-by: Bradley Boutcher <btboutcher@icloud.com>
This commit is contained in:
Yoshiyuki Mineo
2023-05-04 00:08:20 +09:00
committed by GitHub
parent cc94b60628
commit 597171da2e
4 changed files with 145 additions and 6 deletions

35
mock/sonyflake_mock.go Normal file
View File

@@ -0,0 +1,35 @@
// Package mock offers implementations of interfaces defined in types.go
// This allows complete control over input / output for any given method that consumes
// a given type
package mock
import (
"fmt"
"net"
"github.com/sony/sonyflake/types"
)
// NewSuccessfulInterfaceAddrs returns a single private IP address
func NewSuccessfulInterfaceAddrs() types.InterfaceAddrs {
ifat := make([]net.Addr, 0, 1)
ifat = append(ifat, &net.IPNet{IP: []byte{192, 168, 0, 1}, Mask: []byte{255, 0, 0, 0}})
return func() ([]net.Addr, error) {
return ifat, nil
}
}
// NewFailingInterfaceAddrs returns an error
func NewFailingInterfaceAddrs() types.InterfaceAddrs {
return func() ([]net.Addr, error) {
return nil, fmt.Errorf("test error")
}
}
// NewFailingInterfaceAddrs returns an empty slice of addresses
func NewNilInterfaceAddrs() types.InterfaceAddrs {
return func() ([]net.Addr, error) {
return []net.Addr{}, nil
}
}

View File

@@ -12,6 +12,8 @@ import (
"net"
"sync"
"time"
"github.com/sony/sonyflake/types"
)
// These constants are the bit lengths of Sonyflake ID parts.
@@ -50,6 +52,8 @@ type Sonyflake struct {
machineID uint16
}
var defaultInterfaceAddrs = net.InterfaceAddrs
// NewSonyflake returns a new Sonyflake configured with the given Settings.
// NewSonyflake returns nil in the following cases:
// - Settings.StartTime is ahead of the current time.
@@ -71,7 +75,7 @@ func NewSonyflake(st Settings) *Sonyflake {
var err error
if st.MachineID == nil {
sf.machineID, err = lower16BitPrivateIP()
sf.machineID, err = lower16BitPrivateIP(defaultInterfaceAddrs)
} else {
sf.machineID, err = st.MachineID()
}
@@ -131,8 +135,8 @@ func (sf *Sonyflake) toID() (uint64, error) {
uint64(sf.machineID), nil
}
func privateIPv4() (net.IP, error) {
as, err := net.InterfaceAddrs()
func privateIPv4(interfaceAddrs types.InterfaceAddrs) (net.IP, error) {
as, err := interfaceAddrs()
if err != nil {
return nil, err
}
@@ -156,8 +160,8 @@ func isPrivateIPv4(ip net.IP) bool {
(ip[0] == 10 || ip[0] == 172 && (ip[1] >= 16 && ip[1] < 32) || ip[0] == 192 && ip[1] == 168)
}
func lower16BitPrivateIP() (uint16, error) {
ip, err := privateIPv4()
func lower16BitPrivateIP(interfaceAddrs types.InterfaceAddrs) (uint16, error) {
ip, err := privateIPv4(interfaceAddrs)
if err != nil {
return 0, err
}

View File

@@ -1,10 +1,15 @@
package sonyflake
import (
"bytes"
"fmt"
"net"
"runtime"
"testing"
"time"
"github.com/sony/sonyflake/mock"
"github.com/sony/sonyflake/types"
)
var sf *Sonyflake
@@ -23,7 +28,7 @@ func init() {
startTime = toSonyflakeTime(st.StartTime)
ip, _ := lower16BitPrivateIP()
ip, _ := lower16BitPrivateIP(defaultInterfaceAddrs)
machineID = uint64(ip)
}
@@ -185,6 +190,93 @@ func TestNextIDError(t *testing.T) {
}
}
func TestPrivateIPv4(t *testing.T) {
testCases := []struct {
description string
expected net.IP
interfaceAddrs types.InterfaceAddrs
error string
}{
{
description: "InterfaceAddrs returns an error",
expected: nil,
interfaceAddrs: mock.NewFailingInterfaceAddrs(),
error: "test error",
},
{
description: "InterfaceAddrs returns an empty or nil list",
expected: nil,
interfaceAddrs: mock.NewNilInterfaceAddrs(),
error: "no private ip address",
},
{
description: "InterfaceAddrs returns one or more IPs",
expected: net.IP{192, 168, 0, 1},
interfaceAddrs: mock.NewSuccessfulInterfaceAddrs(),
error: "",
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
actual, err := privateIPv4(tc.interfaceAddrs)
if (err != nil) && (tc.error == "") {
t.Errorf("expected no error, but got: %s", err)
return
} else if (err != nil) && (tc.error != "") {
return
}
if bytes.Equal(actual, tc.expected) {
return
} else {
t.Errorf("error: expected: %s, but got: %s", tc.expected, actual)
}
})
}
}
func TestLower16BitPrivateIP(t *testing.T) {
testCases := []struct {
description string
expected uint16
interfaceAddrs types.InterfaceAddrs
error string
}{
{
description: "InterfaceAddrs returns an empty or nil list",
expected: 0,
interfaceAddrs: mock.NewNilInterfaceAddrs(),
error: "no private ip address",
},
{
description: "InterfaceAddrs returns one or more IPs",
expected: 1,
interfaceAddrs: mock.NewSuccessfulInterfaceAddrs(),
error: "",
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
actual, err := lower16BitPrivateIP(tc.interfaceAddrs)
if (err != nil) && (tc.error == "") {
t.Errorf("expected no error, but got: %s", err)
return
} else if (err != nil) && (tc.error != "") {
return
}
if actual == tc.expected {
return
} else {
t.Errorf("error: expected: %v, but got: %v", tc.expected, actual)
}
})
}
}
func TestSonyflakeTimeUnit(t *testing.T) {
if time.Duration(sonyflakeTimeUnit) != 10*time.Millisecond {
t.Errorf("unexpected time unit")

8
types/types.go Normal file
View File

@@ -0,0 +1,8 @@
// Package Types defines type signatures used throughout SonyFlake. This allows for
// fine-tuned control over imports, and the ability to mock out imports as well
package types
import "net"
// InterfaceAddrs defines the interface used for retrieving network addresses
type InterfaceAddrs func() ([]net.Addr, error)