Skip to content

Commit 02003bc

Browse files
committed
Improve CI handling test coverage and added missing coverage test.
Signed-off-by: Lorenzo Buitizon <[email protected]>
1 parent d353521 commit 02003bc

18 files changed

+1364
-463
lines changed

.github/workflows/ci-testing.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ jobs:
2727
${{ runner.os }}-go-
2828
- name: test
2929
run: make test
30+
- name: Generate coverage report
31+
run: go tool cover -func=coverage.out
32+
- name: Generate HTML coverage report
33+
run: go tool cover -html=coverage.out -o coverage.html
34+
- name: Upload coverage reports as artifacts
35+
uses: actions/upload-artifact@v4
36+
with:
37+
name: coverage-reports
38+
path: |
39+
coverage.out
40+
coverage.html
41+
- name: Upload coverage to Codecov
42+
uses: codecov/codecov-action@v4
43+
with:
44+
file: ./coverage.out
45+
flags: unittests
46+
name: codecov-umbrella
3047

3148
testacc:
3249
runs-on: ubuntu-22.04
@@ -61,3 +78,12 @@ jobs:
6178
run: make -e testacc
6279
env:
6380
NETBOX_VERSION: ${{ matrix.netbox-version }}
81+
- name: Check coverage threshold
82+
if: matrix.netbox-version == 'v4.2.9' # Only check on one version to avoid duplication
83+
run: |
84+
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print substr($3, 1, length($3)-1)}')
85+
echo "Current coverage: $COVERAGE%"
86+
if (( $(echo "$COVERAGE < 75.0" | bc -l) )); then
87+
echo "Coverage dropped below 75%: $COVERAGE%"
88+
exit 1
89+
fi

GNUmakefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ default: testacc
1414
.PHONY: testacc
1515
testacc: docker-up
1616
@echo "⌛ Startup acceptance tests on $(NETBOX_SERVER_URL) with version $(NETBOX_VERSION)"
17-
TF_ACC=1 go test -timeout 20m -v -cover $(TEST)
17+
TF_ACC=1 go test -timeout 20m -v -cover -coverprofile=coverage.out $(TEST)
1818

1919
.PHONY: testacc-specific-test
2020
testacc-specific-test: # docker-up
@@ -24,7 +24,7 @@ testacc-specific-test: # docker-up
2424

2525
.PHONY: test
2626
test:
27-
go test $(TEST) $(TESTARGS) -timeout=120s -parallel=4 -cover
27+
go test $(TEST) $(TESTARGS) -timeout=120s -parallel=4 -cover -coverprofile=coverage.out
2828

2929
# Run dockerized Netbox for acceptance testing
3030
.PHONY: docker-up
@@ -52,3 +52,11 @@ docs:
5252
fmt:
5353
go fmt
5454
go fmt netbox/*.go
55+
56+
.PHONY: coverage
57+
coverage:
58+
./scripts/coverage.sh
59+
60+
.PHONY: coverage-html
61+
coverage-html:
62+
./scripts/coverage.sh html

docs/MOCK_TESTING.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# Mock-Based Testing for Better Coverage
2+
3+
This document outlines how to implement mock-based unit tests to improve test coverage beyond acceptance tests.
4+
5+
## Why Mock-Based Testing?
6+
7+
- **Higher Coverage**: Test error paths and edge cases without live NetBox instance
8+
- **Faster Execution**: No network calls or Docker setup required
9+
- **Reliable**: Tests don't depend on external services
10+
- **Focused**: Test specific functions in isolation
11+
12+
## Current Coverage Status
13+
14+
Current test coverage: ~75.0%
15+
16+
Areas that would benefit from mock tests:
17+
- API error handling in data sources and resources
18+
- Network failure scenarios
19+
- Authentication errors
20+
- Rate limiting
21+
- Malformed responses
22+
23+
## Recommended Mock Testing Setup
24+
25+
### 1. Choose a Mocking Framework
26+
27+
```bash
28+
go get github.com/stretchr/testify/mock
29+
# or
30+
go get github.com/golang/mock/gomock
31+
```
32+
33+
### 2. Example Mock Test Structure
34+
35+
```go
36+
package netbox
37+
38+
import (
39+
"errors"
40+
"testing"
41+
42+
"github.com/fbreckle/go-netbox/netbox/client"
43+
"github.com/fbreckle/go-netbox/netbox/client/extras"
44+
"github.com/fbreckle/go-netbox/netbox/models"
45+
"github.com/stretchr/testify/assert"
46+
)
47+
48+
// Mock client for testing
49+
type mockNetBoxClient struct {
50+
extrasAPI *mockExtrasAPI
51+
}
52+
53+
type mockExtrasAPI struct {
54+
tagsListFunc func(*extras.ExtrasTagsListParams) (*extras.ExtrasTagsListOK, error)
55+
}
56+
57+
func (m *mockExtrasAPI) ExtrasTagsList(params *extras.ExtrasTagsListParams, authInfo interface{}) (*extras.ExtrasTagsListOK, error) {
58+
if m.tagsListFunc != nil {
59+
return m.tagsListFunc(params)
60+
}
61+
return nil, errors.New("mock not implemented")
62+
}
63+
64+
func TestFindTag_APIError(t *testing.T) {
65+
// Setup mock
66+
mockAPI := &mockExtrasAPI{
67+
tagsListFunc: func(params *extras.ExtrasTagsListParams) (*extras.ExtrasTagsListOK, error) {
68+
return nil, errors.New("connection refused")
69+
},
70+
}
71+
72+
mockClient := &client.NetBoxAPI{}
73+
// Note: In practice, you'd need to properly inject the mock
74+
75+
// This is a simplified example - actual implementation would require
76+
// interface extraction and dependency injection
77+
tag, err := findTag(mockClient, "test-tag")
78+
79+
assert.Error(t, err)
80+
assert.Nil(t, tag)
81+
assert.Contains(t, err.Error(), "API Error")
82+
}
83+
```
84+
85+
### 3. Implementation Strategy
86+
87+
1. **Extract Interfaces**: Create interfaces for API clients to enable mocking
88+
2. **Dependency Injection**: Modify functions to accept interfaces instead of concrete types
89+
3. **Mock Generation**: Use code generation tools to create mocks
90+
4. **Test Organization**: Separate unit tests from acceptance tests
91+
92+
### 4. Functions to Mock Test
93+
94+
Priority order for mock testing:
95+
96+
1. **Utility Functions** (already done)
97+
- `findTag` in `tags.go`
98+
- `getNestedTagListFromResourceDataSet`
99+
- `readTags`
100+
101+
2. **Data Source Functions**
102+
- `dataSourceNetboxAsnsRead` - API errors, no results
103+
- `dataSourceNetboxTagRead` - multiple results, API errors
104+
105+
3. **Resource Functions**
106+
- CRUD operations with API failures
107+
- 404 handling in read/delete
108+
- Validation errors
109+
110+
4. **Provider Functions**
111+
- `providerConfigure` with invalid credentials
112+
- Version checking failures
113+
114+
### 5. Benefits Expected
115+
116+
- Coverage increase: 74.8% → 85%+
117+
- Faster test execution
118+
- Better error path testing
119+
- Reduced CI resource usage
120+
121+
## Getting Started
122+
123+
1. Start with simple functions like `findTag`
124+
2. Extract interfaces for API clients
125+
3. Use testify/mock for simple mocking
126+
4. Gradually expand to more complex scenarios
127+
128+
## Example Implementation
129+
130+
See `tags_mock_test.go` for a basic mock test example (requires interface extraction for full implementation).

netbox/client_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,57 @@ func TestURLMissingAccessKey(t *testing.T) {
6262
assert.Error(t, err)
6363
}
6464

65+
func TestClientWithCustomHeaders(t *testing.T) {
66+
config := Config{
67+
APIToken: "07b12b765127747e4afd56cb531b7bf9c61f3c30",
68+
ServerURL: "https://localhost:8080",
69+
Headers: map[string]interface{}{
70+
"X-Custom-Header": "test-value",
71+
"X-Another-Header": 123,
72+
},
73+
}
74+
75+
client, err := config.Client()
76+
assert.NotNil(t, client)
77+
assert.NoError(t, err)
78+
}
79+
80+
func TestClientWithInsecureHTTPS(t *testing.T) {
81+
config := Config{
82+
APIToken: "07b12b765127747e4afd56cb531b7bf9c61f3c30",
83+
ServerURL: "https://localhost:8080",
84+
AllowInsecureHTTPS: true,
85+
}
86+
87+
client, err := config.Client()
88+
assert.NotNil(t, client)
89+
assert.NoError(t, err)
90+
}
91+
92+
func TestClientWithRequestTimeout(t *testing.T) {
93+
config := Config{
94+
APIToken: "07b12b765127747e4afd56cb531b7bf9c61f3c30",
95+
ServerURL: "https://localhost:8080",
96+
RequestTimeout: 30,
97+
}
98+
99+
client, err := config.Client()
100+
assert.NotNil(t, client)
101+
assert.NoError(t, err)
102+
}
103+
104+
func TestClientWithStripTrailingSlashes(t *testing.T) {
105+
config := Config{
106+
APIToken: "07b12b765127747e4afd56cb531b7bf9c61f3c30",
107+
ServerURL: "https://localhost:8080/",
108+
StripTrailingSlashesFromURL: true,
109+
}
110+
111+
client, err := config.Client()
112+
assert.NotNil(t, client)
113+
assert.NoError(t, err)
114+
}
115+
65116
func TestAdditionalHeadersSet(t *testing.T) {
66117
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
67118
vals, ok := r.Header["Hello"]
@@ -87,6 +138,32 @@ func TestAdditionalHeadersSet(t *testing.T) {
87138
client.Status.StatusList(req, nil)
88139
}
89140

141+
func TestCustomHeaderTransportRoundTrip(t *testing.T) {
142+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
143+
vals, ok := r.Header["Custom-Header"]
144+
145+
assert.True(t, ok)
146+
assert.Len(t, vals, 1)
147+
assert.Equal(t, vals[0], "test-value")
148+
w.WriteHeader(http.StatusOK)
149+
}))
150+
defer ts.Close()
151+
152+
// Create a custom header transport
153+
transport := customHeaderTransport{
154+
original: http.DefaultTransport,
155+
headers: map[string]interface{}{
156+
"Custom-Header": "test-value",
157+
},
158+
}
159+
160+
req, _ := http.NewRequest("GET", ts.URL, nil)
161+
resp, err := transport.RoundTrip(req)
162+
163+
assert.NoError(t, err)
164+
assert.Equal(t, http.StatusOK, resp.StatusCode)
165+
}
166+
90167
/* TODO
91168
func TestInvalidHttpsCertificate(t *testing.T) {}
92169
*/

netbox/custom_fields_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package netbox
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestGetCustomFields(t *testing.T) {
10+
// Test with valid map
11+
input := map[string]interface{}{
12+
"field1": "value1",
13+
"field2": "value2",
14+
}
15+
result := getCustomFields(input)
16+
assert.Equal(t, input, result)
17+
18+
// Test with empty map
19+
emptyInput := map[string]interface{}{}
20+
result2 := getCustomFields(emptyInput)
21+
assert.Nil(t, result2)
22+
23+
// Test with nil
24+
result3 := getCustomFields(nil)
25+
assert.Nil(t, result3)
26+
27+
// Test with non-map type
28+
result4 := getCustomFields("not a map")
29+
assert.Nil(t, result4)
30+
}

netbox/data_source_netbox_asns_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package netbox
22

33
import (
44
"fmt"
5+
"regexp"
56
"testing"
67

78
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
@@ -65,6 +66,26 @@ data "netbox_asns" "test" {
6566
}`
6667
}
6768

69+
func testAccNetboxAsnsInvalidFilter() string {
70+
return `
71+
data "netbox_asns" "test" {
72+
filter {
73+
name = "invalid_filter"
74+
value = "test"
75+
}
76+
}`
77+
}
78+
79+
func testAccNetboxAsnsNoResults() string {
80+
return `
81+
data "netbox_asns" "test" {
82+
filter {
83+
name = "asn"
84+
value = "999999"
85+
}
86+
}`
87+
}
88+
6889
func TestAccNetboxAsnsDataSource_basic(t *testing.T) {
6990
testName := testAccGetTestName("asns_ds_basic")
7091
setUp := testAccNetboxAsnsSetUp(testName)
@@ -104,3 +125,27 @@ func TestAccNetboxAsnsDataSource_basic(t *testing.T) {
104125
},
105126
})
106127
}
128+
129+
func TestAccNetboxAsnsDataSource_invalidFilter(t *testing.T) {
130+
resource.Test(t, resource.TestCase{
131+
Providers: testAccProviders,
132+
Steps: []resource.TestStep{
133+
{
134+
Config: testAccNetboxAsnsInvalidFilter(),
135+
ExpectError: regexp.MustCompile("'invalid_filter' is not a supported filter parameter"),
136+
},
137+
},
138+
})
139+
}
140+
141+
func TestAccNetboxAsnsDataSource_noResults(t *testing.T) {
142+
resource.Test(t, resource.TestCase{
143+
Providers: testAccProviders,
144+
Steps: []resource.TestStep{
145+
{
146+
Config: testAccNetboxAsnsNoResults(),
147+
ExpectError: regexp.MustCompile("no result"),
148+
},
149+
},
150+
})
151+
}

0 commit comments

Comments
 (0)