Posted on • Edited on • Originally published atjdheyburn.co.uk
Extending Gotests for Strict Error Tests
This is my first post on dev.to, X-posted from my new personal blog which can be foundhere. Hopefully I'll have dev.to publish from RSS
feeds once I've worked it out!
Happy to receive any feedback you may have! 😃
Strict Error Tests in Java
I love confirming the stability of my code through writing tests and practicing Test-driven development (TDD). For Java, JUnit was my preferred testing framework of choice. When writing tests to confirm an exception had been thrown, I used the optional parameterexpected
for the annotation@Test
, however I quickly found that this solution would not work for methods where I raised the same exception class multiple times for different error messages, and testing on those messages.
This is commonly found in writing a validation method such as the one below, which will take in a name of a dog and return a boolean if it is valid.
publicstaticbooleanvalidateDogName(StringdogName)throwsDogValidationException{if(containsSymbols(dogName)){thrownewDogValidationException("Dogs cannot have symbols in their name!");}if(dogName.length>100){thrownewDogValidationException("Who has a name for a dog that long?!");}returntrue;}
For this method, just using@Test(expected = DogValidationException.class)
on our test method is not sufficient; how can we determine that the exception was raised for a dogName.length breach and not for containing symbols?
In order for me to resolve this, I came across theExpectedException
class for JUnit onBaeldung which enables us to specify the error message expected. Here it is applied to the test case for this method:
@RulepublicExpectedExceptionexceptionRule=ExpectedException.none();@TestpublicvoidshouldHandleDogNameWithSymbols(){exceptionRule.expect(DogValidationException.class);exceptionRule.expectMessage("Dogs cannot have symbols in their name!");validateDogName("GoodestBoy#1");}
Applying to Golang
Back to Golang, there is a built-in library aptly namedtesting
which enables us to assert on test conditions. When combined withGotests - a tool for generating Go tests from your code - writing tests could not be easier! I love how this is bundled in with the Go extension for VSCode, my text editor of choice (for now...).
Converting the above JavavalidateDogName
method to Golang will produce something like:
funcvalidateDogName(namestring)(bool,error){ifcontainsSymbols(name){returnfalse,errors.New("dog cannot have symbols in their name")}iflen(name)>100{returnfalse,errors.New("who has a name for a dog that long")}returntrue,nil}
If you have a Go method that returns theerror
interface, then gotests will generate a test that look like this:
funcTest_validateDogName(t*testing.T){typeargsstruct{namestring}tests:=[]struct{namestringargsargswantboolwantErrbool}{name:"Test error was thrown for dog name with symbols",args:args{name:"GoodestBoy#1",},want:false,wantErr:true,}for_,tt:=rangetests{t.Run(tt.name,func(t*testing.T){got,err:=validateDogName(tt.args.name)if(err!=nil)!=tt.wantErr{t.Errorf("validateDogName() error = %v, wantErr %v",err,tt.wantErr)return}ifgot!=tt.want{t.Errorf("validateDogName() = %v, want %v",got,tt.want)}})}}
From the above we are limited to what error we can assert for, hereany error returned will pass the test. This is equivalent to using@Test(expected=Exception.class)
in JUnit! But there is another way...
Modifying the Generated Test
We only need to make a few simple changes to the generated test to give us the ability to assert on test error message...
funcTest_validateDogName(t*testing.T){typeargsstruct{namestring}tests:=[]struct{namestringargsargswantboolwantErrerror}{name:"Test error was thrown for dog name with symbols",args:args{name:"GoodestBoy#1",},want:false,wantErr:errors.New("dog cannot have symbols in their name"),}for_,tt:=rangetests{t.Run(tt.name,func(t*testing.T){got,err:=validateDogName(tt.args.name)iftt.wantErr!=nil&&!reflect.DeepEqual(err,tt.wantErr){t.Errorf("validateDogName() error = %v, wantErr %v",err,tt.wantErr)return}ifgot!=tt.want{t.Errorf("validateDogName() = %v, want %v",got,tt.want)}})}}
From the above there are three changes, let's go over them individually:
wantErr error
- we are changing this from
bool
so that we can make a comparison against the error returned from the function
- we are changing this from
wantErr: errors.New("dog cannot have symbols in their name"),
- this is the error struct that we are expecting
if tt.wantErr != nil && !reflect.DeepEqual(err, tt.wantErr) {
- check to make sure the test is expected an error, if so then compare it against the returned error
Point 3 provides additional support if there was a test case that did not expect an error. Note howwantErr
is omitted entirely from the test case below.
{name:"Should return true for valid dog name",args:args{name:"Benedict Cumberland the Sausage Dog",},want:true,}
Customising Gotests Generated Test
Gotests gives us the ability to provide our own templates for generating tests, and can easily be integrated into your text editor of choice. I'll show you how this can be done in VSCode.
Check out gotests and copy the templates directory to a place of your choosing
git clone https://github.com/cweill/gotests.git
cp -R gotests/internal/render/templates ~/scratch/gotests
Overwrite the contents of function.tmpl withthe contents of this Gist
Add the following setting to VSCode's settings.json
"go.generateTestsFlags": ["--template_dir=~/scratch/templates"]
Once you have done that, future tests will now generate with stricter error testing! 🎉
Closing
I understand that the recommendations above will make your code more fragile, as the code is subject to any changing of the error message of say a downstream library. However for myself, I prefer to write tests that are strict and minimalise the chance of other errors contaminating tests.
I also understand that GoodestBoy#1 is probably a valid name for a dog! 🐶
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse