Repository: hetznercloud/hcloud-go
Pull request #637 (2025-03-20 14:06:48)

feat: consistent argument errors
A new error used to streamline missing arguments errors or invalid values during validation in our library.

Related to #556

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/637?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) Report
Attention: Patch coverage is `97.05882%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 79.08%. Comparing base [(`c7d30ed`)](https://app.codecov.io/gh/hetznercloud/hcloud-go/commit/c7d30ede2ca3bb1837c269077d8fef2a3b0226d8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) to head [(`3d9e251`)](https://app.codecov.io/gh/hetznercloud/hcloud-go/commit/3d9e251ae0eef9a267fe7ad5eb5172c75117bace?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).

| [Files with missing lines](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/637?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) | Patch % | Lines |
|---|---|---|
| [hcloud/certificate.go](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/637?src=pr&el=tree&filepath=hcloud%2Fcertificate.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud#diff-aGNsb3VkL2NlcnRpZmljYXRlLmdv) | 80.00% | [1 Missing :warning: ](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/637?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #637      +/-   ##
==========================================
+ Coverage   79.06%   79.08%   +0.01%     
==========================================
  Files          50       50              
  Lines        4399     4427      +28     
==========================================
+ Hits         3478     3501      +23     
- Misses        705      708       +3     
- Partials      216      218       +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/hetznercloud/hcloud-go/pull/637?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=hetznercloud).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/hcloud/certificate.go b/hcloud/certificate.go
index a57d9c5..c33b304 100644
--- a/hcloud/certificate.go
+++ b/hcloud/certificate.go
@@ -2,7 +2,6 @@ package hcloud
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net/url"
 	"time"
@@ -186,7 +185,7 @@ type CertificateCreateOpts struct {
 // Validate checks if options are valid.
 func (o CertificateCreateOpts) Validate() error {
 	if o.Name == "" {
-		return errors.New("missing name")
+		return missingField(o, "Name")
 	}
 	switch o.Type {
 	case "", CertificateTypeUploaded:
@@ -194,23 +193,23 @@ func (o CertificateCreateOpts) Validate() error {
 	case CertificateTypeManaged:
 		return o.validateManaged()
 	default:
-		return fmt.Errorf("invalid type: %s", o.Type)
+		return invalidFieldValue(o, "Type", o.Type)
 	}
 }
 
 func (o CertificateCreateOpts) validateManaged() error {
 	if len(o.DomainNames) == 0 {
-		return errors.New("no domain names")
+		return missingField(o, "DomainNames")
 	}
 	return nil
 }
 
 func (o CertificateCreateOpts) validateUploaded() error {
 	if o.Certificate == "" {
-		return errors.New("missing certificate")
+		return missingField(o, "Certificate")
 	}
 	if o.PrivateKey == "" {
-		return errors.New("missing private key")
+		return missingField(o, "PrivateKey")
 	}
 	return nil
 }
@@ -221,7 +220,7 @@ func (o CertificateCreateOpts) validateUploaded() error {
 // CreateCertificate to create such certificates.
 func (c *CertificateClient) Create(ctx context.Context, opts CertificateCreateOpts) (*Certificate, *Response, error) {
 	if !(opts.Type == "" || opts.Type == CertificateTypeUploaded) {
-		return nil, nil, fmt.Errorf("invalid certificate type: %s", opts.Type)
+		return nil, nil, invalidFieldValue(opts, "Type", opts.Type)
 	}
 	result, resp, err := c.CreateCertificate(ctx, opts)
 	if err != nil {
@@ -258,7 +257,7 @@ func (c *CertificateClient) CreateCertificate(
 		reqBody.Type = string(CertificateTypeManaged)
 		reqBody.DomainNames = opts.DomainNames
 	default:
-		return CertificateCreateResult{}, nil, fmt.Errorf("invalid certificate type: %v", opts.Type)
+		return result, nil, invalidFieldValue(opts, "Type", opts.Type)
 	}
 
 	if opts.Labels != nil {
diff --git a/hcloud/error.go b/hcloud/error.go
index a3aa446..eb3ae4f 100644
--- a/hcloud/error.go
+++ b/hcloud/error.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"net"
 	"slices"
+	"strings"
 )
 
 // ErrorCode represents an error code returned from the API.
@@ -155,3 +156,36 @@ type DNSNotFoundError struct {
 func (e DNSNotFoundError) Error() string {
 	return fmt.Sprintf("dns for ip %s not found", e.IP.String())
 }
+
+// ArgumentError is a type of error returned when validating arguments.
+type ArgumentError string
+
+func (e ArgumentError) Error() string { return string(e) }
+
+func newArgumentErrorf(format string, args ...any) ArgumentError {
+	return ArgumentError(fmt.Sprintf(format, args...))
+}
+
+func missingArgument(obj any) error {
+	return newArgumentErrorf("missing argument [%T]", obj)
+}
+
+func missingField(obj any, field string) error {
+	return newArgumentErrorf("missing field [%s] in [%T]", field, obj)
+}
+
+func invalidFieldValue(obj any, field string, value any) error {
+	return newArgumentErrorf("invalid value '%v' for field [%s] in [%T]", value, field, obj)
+}
+
+func missingOneOfFields(obj any, fields ...string) error {
+	return newArgumentErrorf("missing one of fields [%s] in [%T]", strings.Join(fields, ", "), obj)
+}
+
+func mutuallyExclusiveFields(obj any, fields ...string) error {
+	return newArgumentErrorf("found mutually exclusive fields [%s] in [%T]", strings.Join(fields, ", "), obj)
+}
+
+func missingRequiredTogetherFields(obj any, fields ...string) error {
+	return newArgumentErrorf("missing required together fields [%s] in [%T]", strings.Join(fields, ", "), obj)
+}
diff --git a/hcloud/firewall.go b/hcloud/firewall.go
index b69e6b2..abfe438 100644
--- a/hcloud/firewall.go
+++ b/hcloud/firewall.go
@@ -2,7 +2,6 @@ package hcloud
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net"
 	"net/url"
@@ -183,7 +182,7 @@ type FirewallCreateOpts struct {
 // Validate checks if options are valid.
 func (o FirewallCreateOpts) Validate() error {
 	if o.Name == "" {
-		return errors.New("missing name")
+		return missingField(o, "Name")
 	}
 	return nil
 }
diff --git a/hcloud/floating_ip.go b/hcloud/floating_ip.go
index 59cd816..bf190c4 100644
--- a/hcloud/floating_ip.go
+++ b/hcloud/floating_ip.go
@@ -2,7 +2,6 @@ package hcloud
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net"
 	"net/url"
@@ -184,10 +183,10 @@ func (o FloatingIPCreateOpts) Validate() error {
 	case FloatingIPTypeIPv4, FloatingIPTypeIPv6:
 		break
 	default:
-		return errors.New("missing or invalid type")
+		return invalidFieldValue(o, "Type", o.Type)
 	}
 	if o.HomeLocation == nil && o.Server == nil {
-		return errors.New("one of home location or server is required")
+		return missingOneOfFields(o, "HomeLocation", "Server")
 	}
 	return nil
 }
diff --git a/hcloud/load_balancer.go b/hcloud/load_balancer.go
index f79ccd8..f41caec 100644
--- a/hcloud/load_balancer.go
+++ b/hcloud/load_balancer.go
@@ -891,13 +891,13 @@ type LoadBalancerGetMetricsOpts struct {
 
 func (o LoadBalancerGetMetricsOpts) Validate() error {
 	if len(o.Types) == 0 {
-		return fmt.Errorf("no metric types specified")
+		return missingField(o, "Types")
 	}
 	if o.Start.IsZero() {
-		return fmt.Errorf("no start time specified")
+		return missingField(o, "Start")
 	}
 	if o.End.IsZero() {
-		return fmt.Errorf("no end time specified")
+		return missingField(o, "End")
 	}
 	return nil
 }
@@ -941,7 +941,7 @@ func (c *LoadBalancerClient) GetMetrics(
 	ctx = ctxutil.SetOpPath(ctx, opPath)
 
 	if loadBalancer == nil {
-		return nil, nil, fmt.Errorf("illegal argument: load balancer is nil")
+		return nil, nil, missingArgument(loadBalancer)
 	}
 
 	if err := opts.Validate(); err != nil {
diff --git a/hcloud/network.go b/hcloud/network.go
index 871e06f..f33f253 100644
--- a/hcloud/network.go
+++ b/hcloud/network.go
@@ -2,7 +2,6 @@ package hcloud
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net"
 	"net/url"
@@ -222,10 +221,10 @@ type NetworkCreateOpts struct {
 // Validate checks if options are valid.
 func (o NetworkCreateOpts) Validate() error {
 	if o.Name == "" {
-		return errors.New("missing name")
+		return missingField(o, "Name")
 	}
 	if o.IPRange == nil || o.IPRange.String() == "" {
-		return errors.New("missing IP range")
+		return missingField(o, "IPRange")
 	}
 	return nil
 }
diff --git a/hcloud/placement_group.go b/hcloud/placement_group.go
index 3af0f77..9176b3d 100644
--- a/hcloud/placement_group.go
+++ b/hcloud/placement_group.go
@@ -2,7 +2,6 @@ package hcloud
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net/url"
 	"time"
@@ -134,7 +133,7 @@ type PlacementGroupCreateOpts struct {
 // Validate checks if options are valid.
 func (o PlacementGroupCreateOpts) Validate() error {
 	if o.Name == "" {
-		return errors.New("missing name")
+		return missingField(o, "Name")
 	}
 	return nil
 }
diff --git a/hcloud/server.go b/hcloud/server.go
index 06ba94e..d8377a1 100644
--- a/hcloud/server.go
+++ b/hcloud/server.go
@@ -2,7 +2,6 @@ package hcloud
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net"
 	"net/url"
@@ -307,22 +306,16 @@ type ServerCreateFirewall struct {
 // Validate checks if options are valid.
 func (o ServerCreateOpts) Validate() error {
 	if o.Name == "" {
-		return errors.New("missing name")
+		return missingField(o, "Name")
 	}
 	if o.ServerType == nil || (o.ServerType.ID == 0 && o.ServerType.Name == "") {
-		return errors.New("missing server type")
+		return missingField(o, "ServerType")
 	}
 	if o.Image == nil || (o.Image.ID == 0 && o.Image.Name == "") {
-		return errors.New("missing image")
+		return missingField(o, "Image")
 	}
 	if o.Location != nil && o.Datacenter != nil {
-		return errors.New("location and datacenter are mutually exclusive")
-	}
-	if o.PublicNet != nil {
-		if !o.PublicNet.EnableIPv4 && !o.PublicNet.EnableIPv6 &&
-			len(o.Networks) == 0 && (o.StartAfterCreate == nil || *o.StartAfterCreate) {
-			return errors.New("missing networks or StartAfterCreate == false when EnableIPv4 and EnableIPv6 is false")
-		}
+		return mutuallyExclusiveFields(o, "Location", "Datacenter")
 	}
 	return nil
 }
@@ -598,7 +591,7 @@ func (o ServerCreateImageOpts) Validate() error {
 	case "":
 		break
 	default:
-		return errors.New("invalid type")
+		return invalidFieldValue(o, "Type", o.Type)
 	}
 
 	return nil
@@ -622,7 +615,7 @@ func (c *ServerClient) CreateImage(ctx context.Context, server *Server, opts *Se
 	reqBody := schema.ServerActionCreateImageRequest{}
 	if opts != nil {
 		if err := opts.Validate(); err != nil {
-			return result, nil, fmt.Errorf("invalid options: %w", err)
+			return result, nil, err
 		}
 		if opts.Description != nil {
 			reqBody.Description = opts.Description
@@ -1013,13 +1006,13 @@ type ServerGetMetricsOpts struct {
 
 func (o ServerGetMetricsOpts) Validate() error {
 	if len(o.Types) == 0 {
-		return fmt.Errorf("no metric types specified")
+		return missingField(o, "Types")
 	}
 	if o.Start.IsZero() {
-		return fmt.Errorf("no start time specified")
+		return missingField(o, "Start")
 	}
 	if o.End.IsZero() {
-		return fmt.Errorf("no end time specified")
+		return missingField(o, "End")
 	}
 	return nil
 }
@@ -1061,7 +1054,7 @@ func (c *ServerClient) GetMetrics(ctx context.Context, server *Server, opts Serv
 	ctx = ctxutil.SetOpPath(ctx, opPath)
 
 	if server == nil {
-		return nil, nil, fmt.Errorf("illegal argument: server is nil")
+		return nil, nil, missingArgument(server)
 	}
 
 	if err := opts.Validate(); err != nil {
diff --git a/hcloud/ssh_key.go b/hcloud/ssh_key.go
index 7c7f9ef..7b1e8cc 100644
--- a/hcloud/ssh_key.go
+++ b/hcloud/ssh_key.go
@@ -2,7 +2,6 @@ package hcloud
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net/url"
 	"time"
@@ -127,10 +126,10 @@ type SSHKeyCreateOpts struct {
 // Validate checks if options are valid.
 func (o SSHKeyCreateOpts) Validate() error {
 	if o.Name == "" {
-		return errors.New("missing name")
+		return missingField(o, "Name")
 	}
 	if o.PublicKey == "" {
-		return errors.New("missing public key")
+		return missingField(o, "PublicKey")
 	}
 	return nil
 }
diff --git a/hcloud/volume.go b/hcloud/volume.go
index fbab6c3..b606eba 100644
--- a/hcloud/volume.go
+++ b/hcloud/volume.go
@@ -2,7 +2,6 @@ package hcloud
 
 import (
 	"context"
-	"errors"
 	"fmt"
 	"net/url"
 	"time"
@@ -151,19 +150,19 @@ type VolumeCreateOpts struct {
 // Validate checks if options are valid.
 func (o VolumeCreateOpts) Validate() error {
 	if o.Name == "" {
-		return errors.New("missing name")
+		return missingField(o, "Name")
 	}
 	if o.Size <= 0 {
-		return errors.New("size must be greater than 0")
+		return invalidFieldValue(o, "Size", o.Size)
 	}
 	if o.Server == nil && o.Location == nil {
-		return errors.New("one of server or location must be provided")
+		return missingOneOfFields(o, "Server", "Location")
 	}
 	if o.Server != nil && o.Location != nil {
-		return errors.New("only one of server or location must be provided")
+		return mutuallyExclusiveFields(o, "Server", "Location")
 	}
 	if o.Server == nil && (o.Automount != nil && *o.Automount) {
-		return errors.New("server must be provided when automount is true")
+		return missingRequiredTogetherFields(o, "Automount", "Server")
 	}
 	return nil
 }


Test patch:
diff --git a/hcloud/certificate_test.go b/hcloud/certificate_test.go
index 815a4ca..2d4ba3c 100644
--- a/hcloud/certificate_test.go
+++ b/hcloud/certificate_test.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
 )
@@ -25,7 +26,7 @@ func TestCertificateCreateOptsValidate_Uploaded(t *testing.T) {
 				PrivateKey:  "key",
 				Labels:      map[string]string{},
 			},
-			errMsg: "missing name",
+			errMsg: "missing field [Name] in [hcloud.CertificateCreateOpts]",
 		},
 		{
 			name: "no certificate",
@@ -34,7 +35,7 @@ func TestCertificateCreateOptsValidate_Uploaded(t *testing.T) {
 				PrivateKey: "key",
 				Labels:     map[string]string{},
 			},
-			errMsg: "missing certificate",
+			errMsg: "missing field [Certificate] in [hcloud.CertificateCreateOpts]",
 		},
 		{
 			name: "no private key",
@@ -43,7 +44,7 @@ func TestCertificateCreateOptsValidate_Uploaded(t *testing.T) {
 				Certificate: "cert",
 				Labels:      map[string]string{},
 			},
-			errMsg: "missing private key",
+			errMsg: "missing field [PrivateKey] in [hcloud.CertificateCreateOpts]",
 		},
 		{
 			name: "valid without type",
@@ -90,7 +91,7 @@ func TestCertificateCreateOptsValidate_Managed(t *testing.T) {
 				Type:        "managed",
 				DomainNames: []string{"*.example.com", "example.com"},
 			},
-			errMsg: "missing name",
+			errMsg: "missing field [Name] in [hcloud.CertificateCreateOpts]",
 		},
 		{
 			name: "missing domains",
@@ -98,7 +99,7 @@ func TestCertificateCreateOptsValidate_Managed(t *testing.T) {
 				Name: "I have no domains",
 				Type: "managed",
 			},
-			errMsg: "no domain names",
+			errMsg: "missing field [DomainNames] in [hcloud.CertificateCreateOpts]",
 		},
 		{
 			name: "valid certificate",
@@ -125,7 +126,7 @@ func TestCertificateCreateOptsValidate_Managed(t *testing.T) {
 func TestCertificateCreateOptsValidate_InvalidType(t *testing.T) {
 	opts := CertificateCreateOpts{Name: "invalid type", Type: "invalid"}
 	err := opts.Validate()
-	assert.EqualError(t, err, "invalid type: invalid")
+	assert.EqualError(t, err, "invalid value 'invalid' for field [Type] in [hcloud.CertificateCreateOpts]")
 }
 
 func TestCertificateClientGetByID(t *testing.T) {
@@ -410,9 +411,8 @@ func TestCertificateCreateValidation(t *testing.T) {
 	ctx := context.Background()
 	opts := CertificateCreateOpts{}
 	_, _, err := env.Client.Certificate.Create(ctx, opts)
-	if err == nil || err.Error() != "missing name" {
-		t.Fatalf("unexpected error: %v", err)
-	}
+
+	require.EqualError(t, err, "missing field [Name] in [hcloud.CertificateCreateOpts]")
 }
 
 func TestCertificateDelete(t *testing.T) {
diff --git a/hcloud/error_test.go b/hcloud/error_test.go
index 34bc5c6..502d615 100644
--- a/hcloud/error_test.go
+++ b/hcloud/error_test.go
@@ -140,3 +140,42 @@ func TestIsError(t *testing.T) {
 		})
 	}
 }
+
+func TestArgumentError(t *testing.T) {
+	type Something struct{ Name string }
+	something := Something{Name: "hello"}
+
+	for _, tt := range []struct {
+		err  error
+		want string
+	}{
+		{
+			err:  missingArgument(something),
+			want: "missing argument [hcloud.Something]",
+		},
+		{
+			err:  missingField(something, "Name"),
+			want: "missing field [Name] in [hcloud.Something]",
+		},
+		{
+			err:  invalidFieldValue(something, "Name", something.Name),
+			want: "invalid value 'hello' for field [Name] in [hcloud.Something]",
+		},
+		{
+			err:  missingOneOfFields(something, "Name", "Type"),
+			want: "missing one of fields [Name, Type] in [hcloud.Something]",
+		},
+		{
+			err:  mutuallyExclusiveFields(something, "Name", "Type"),
+			want: "found mutually exclusive fields [Name, Type] in [hcloud.Something]",
+		},
+		{
+			err:  missingRequiredTogetherFields(something, "Name", "Type"),
+			want: "missing required together fields [Name, Type] in [hcloud.Something]",
+		},
+	} {
+		t.Run("", func(t *testing.T) {
+			assert.EqualError(t, tt.err, tt.want)
+		})
+	}
+}
diff --git a/hcloud/firewall_test.go b/hcloud/firewall_test.go
index 7d35b95..a8a806e 100644
--- a/hcloud/firewall_test.go
+++ b/hcloud/firewall_test.go
@@ -8,6 +8,7 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/stretchr/testify/require"
 
 	"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
 )
@@ -289,9 +290,8 @@ func TestFirewallCreateValidation(t *testing.T) {
 	ctx := context.Background()
 	opts := FirewallCreateOpts{}
 	_, _, err := env.Client.Firewall.Create(ctx, opts)
-	if err == nil || err.Error() != "missing name" {
-		t.Fatalf("unexpected error: %v", err)
-	}
+
+	require.EqualError(t, err, "missing field [Name] in [hcloud.FirewallCreateOpts]")
 }
 
 func TestFirewallDelete(t *testing.T) {
diff --git a/hcloud/load_balancer_test.go b/hcloud/load_balancer_test.go
index 26176d4..8cea823 100644
--- a/hcloud/load_balancer_test.go
+++ b/hcloud/load_balancer_test.go
@@ -1155,7 +1155,7 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
 				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
-			expectedErr: "no metric types specified",
+			expectedErr: "missing field [Types] in [hcloud.LoadBalancerGetMetricsOpts]",
 		},
 		{
 			name: "no start time",
@@ -1164,7 +1164,7 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 				Types: []LoadBalancerMetricType{LoadBalancerMetricBandwidth},
 				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
-			expectedErr: "no start time specified",
+			expectedErr: "missing field [Start] in [hcloud.LoadBalancerGetMetricsOpts]",
 		},
 		{
 			name: "no end time",
@@ -1173,7 +1173,7 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 				Types: []LoadBalancerMetricType{LoadBalancerMetricBandwidth},
 				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
 			},
-			expectedErr: "no end time specified",
+			expectedErr: "missing field [End] in [hcloud.LoadBalancerGetMetricsOpts]",
 		},
 		{
 			name: "call to backend API fails",
@@ -1193,7 +1193,7 @@ func TestLoadBalancerGetMetrics(t *testing.T) {
 				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
 				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
-			expectedErr: "illegal argument: load balancer is nil",
+			expectedErr: "missing argument [*hcloud.LoadBalancer]",
 		},
 	}
 
diff --git a/hcloud/network_test.go b/hcloud/network_test.go
index 2c325c6..231e290 100644
--- a/hcloud/network_test.go
+++ b/hcloud/network_test.go
@@ -7,6 +7,8 @@ import (
 	"net/http"
 	"testing"
 
+	"github.com/stretchr/testify/require"
+
 	"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
 )
 
@@ -206,9 +208,7 @@ func TestNetworkCreate(t *testing.T) {
 
 		opts := NetworkCreateOpts{}
 		_, _, err := env.Client.Network.Create(ctx, opts)
-		if err == nil || err.Error() != "missing name" {
-			t.Fatalf("Network.Create should fail with \"missing name\" but failed with %s", err)
-		}
+		require.EqualError(t, err, "missing field [Name] in [hcloud.NetworkCreateOpts]")
 	})
 
 	t.Run("missing required field ip range", func(t *testing.T) {
@@ -219,9 +219,7 @@ func TestNetworkCreate(t *testing.T) {
 			Name: "my-network",
 		}
 		_, _, err := env.Client.Network.Create(ctx, opts)
-		if err == nil || err.Error() != "missing IP range" {
-			t.Fatalf("Network.Create should fail with \"missing IP range\" but failed with %s", err)
-		}
+		require.EqualError(t, err, "missing field [IPRange] in [hcloud.NetworkCreateOpts]")
 	})
 
 	t.Run("required fields", func(t *testing.T) {
diff --git a/hcloud/server_test.go b/hcloud/server_test.go
index 5ccbd6e..7ad746f 100644
--- a/hcloud/server_test.go
+++ b/hcloud/server_test.go
@@ -2242,7 +2242,7 @@ func TestServerGetMetrics(t *testing.T) {
 				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
 				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
-			expectedErr: "no metric types specified",
+			expectedErr: "missing field [Types] in [hcloud.ServerGetMetricsOpts]",
 		},
 		{
 			name:   "no start time",
@@ -2251,7 +2251,7 @@ func TestServerGetMetrics(t *testing.T) {
 				Types: []ServerMetricType{ServerMetricCPU},
 				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
-			expectedErr: "no start time specified",
+			expectedErr: "missing field [Start] in [hcloud.ServerGetMetricsOpts]",
 		},
 		{
 			name:   "no end time",
@@ -2260,7 +2260,7 @@ func TestServerGetMetrics(t *testing.T) {
 				Types: []ServerMetricType{ServerMetricCPU},
 				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
 			},
-			expectedErr: "no end time specified",
+			expectedErr: "missing field [End] in [hcloud.ServerGetMetricsOpts]",
 		},
 		{
 			name:   "call to backend API fails",
@@ -2280,7 +2280,7 @@ func TestServerGetMetrics(t *testing.T) {
 				Start: mustParseTime(t, "2017-01-01T00:00:00Z"),
 				End:   mustParseTime(t, "2017-01-01T23:00:00Z"),
 			},
-			expectedErr: "illegal argument: server is nil",
+			expectedErr: "missing argument [*hcloud.Server]",
 		},
 	}