Monthly Archives: March 2016

TestNG and NetBeans: testing exceptions with externalized messages

It all started with two things that should have been obvious for me for a long time:

  1. Every message in a program should be localizable. For Java programs, that means externalized using a resource bundle. And that includes exceptions. Even if those are never shown to users (it’s debatable whether it’s a good or bad idea), keeping them separated from code is still a good idea. For one thing, it makes documenting error messages much easier!
  2. Every unit should have a unit test. In the extreme case, adhering to XP/TDD principles, those should even be written before the actual code.

Now, I’ve been doing unit testing for quite a long time using JUnit, but I never got around to externalizing messages because our software is used internally (and nobody cares). Now for my open source project (ain’t giving a link because it’s nothing there yet) I decided to try TestNG because of its very nice parametrization of tests, and at the same time externalize messages (because who knows, maybe someone someday will want to translate it).

Turned out that these two things don’t mix easily. Here is a short summary of what needs to be done to get everything running. I’m using NetBeans and Maven, but the approach itself may be applied to any setup that uses TestNG and resource bundles. If you’re experienced with your IDE and TestNG you may wish to skip to step 4, where I explain how to use annotation transformers for testing localized exception messages.

Step 1. Configuring NetBeans

This one is tricky for current NetBeans version which is 8.1 and current Maven Surefire plugin version which is 2.19.1 . Assuming you’ve got everything installed (bundled Maven will do), let’s start by creating an example project. Press Ctrl+Shift+N and select Maven—Java Application (the simplest project that can be created). Choose location, sensible group id like name.yourdomain or name.yourdomain.testproject, sensible artifact name like test, and a sensible package name similar to group id, like this:

nb1 nb2

Now switch to the files tab and create a new folder under src called test. Create a subfolder in it called java:

 

nb3

Now it should look like this:nb4

Switching back to the projects tab, you’ll now see the Test Packages element in the tree:nb5These steps were needed to get on with the TDD approach, creating the first test before coding anything. So far it is all easy as long as you follow the Maven conventions (that’s why it’s src/test/java and not something else, this is just a magical incantation). Now create a package under Test Packages with exactly the same name as the source package (name.tachenov.test in my case). Create a class there, say, MyClassTest or MyClassNGTest (both names will allow you to use Ctrl+Shift+T to navigate between MyClass and the test, once both are created) which should look like

Now it doesn’t even compile for two reasons: there is no MyClass, and, which is far more serious, NetBeans has no idea what @Test is. So ask it to create a stub for MyClass inside the source packages (which is a nice feature available since NetBeans 8.1) and add dependency for TestNG.nb6

nb7

Note that I set Scope to test because we obviously don’t need a testing library unless we’re testing. Now this step is somewhat tricky because if you used NetBeans to generate tests in a regular way (code-first) it would automatically add the TestNG dependency… only it would pick up some version NetBeans thinks is right. Well, turns out you need slightly better than that! The version that NetBeans picked doesn’t work quite right with other tools we need.

Now add import for org.testng.annotations.Test and hit Alt+F6. You should see something like

nb9

OK, TDD step 1 complete! We’ve got a failing test. Now we have to fix it! Once you do something like this the test should run fine:

At this time, even if you screw something up, the test will probably run fine nevertheless. Things start to get weird a bit later.

Step 2. Testing exception message

Now we’ve got to break the test again:

Let’s fix it:

Now you have this bad feeling about copy-pasting a message like that. Not only it’s in our code, but it’s in two different places! What if we want to change it? What if we want to translate it? Although Java provides a weird getLocalizedMessage() method for exceptions, it seems to be a real pain to get it working because there is no way to set that localized message! So might as well just localize the non-localized message, even though it feels weird, but that’s what Brian Goetz himself actually recommends!

Step 3. Externalizing the message

Without touching the test, let’s externalize the message in our class. That’s allowed by TDD since it’s the refactoring step: we aren’t changing any logic, we only moving things around.nb10

In the internationalization dialog, press Select, type in a reasonable name, say, “exceptions”, and then press Create New:nb11

Now enter a reasonable key for the message, say, MyClass.objectIsNull, and press Replace and Close.

nb12

After a bit of further refactoring (extracting constants, adding imports) you should get something like this:

One last step is to move the newly created exceptions.properties from src/main/java to src/main/resources, under the same package that it was. Your file structure should look like this:

nb13

This is needed because Maven looks for resources in src/main/resources, but NetBeans created our bundle in src/main/java. So we fixed that and run our test again. We see that it passed, so it’s time to refactor the test.

Step 4. Making the test work with externalized messages

Our first idea may look like this:

We face two problems here. One is that EX_OBJECT_IS_NULL is private. Well, no big deal—just make it package private. Feels weird exposing internals just like that, but package private is still private, and it’s just a constant, so no harm done!

The other problem is far more serious. We get the “element value must be a constant experession” error. Makes sense, since we need it at compile time! But how on earth…? That’s what I asked on Stack Overflow. Thanks to the quick answer I got there, I was able to implement a very nice solution. Like the answer says, create a new annotation type. I call it ExpectedExceptionMessageKey.

And create a class called, say ExceptionRegExpTransformer in our test package.

Now our test should look like this:

I intentionally left expectedExceptionsMessageRegExp there, but changed the message. This allows us to check whether the new way of testing really works. Turns out it isn’t! Well, no big surprise since we never told TestNG to actually use our annotation transformer! At this point, it would be really nice to have it injected magically in our class by using some sort of @AnnotationTransformer annotation on the transformer class. Alas, there is no such magic! Or at least I haven’t found any. So we have to configure TestNG manually through the Surefire plugin. Open pom.xml under Project Files in the Projects view. Insert something like this after </dependencies>:

Whew. This doesn’t look neither elegant, nor short, nor intuitive. For some reason NetBeans stops auto-completing tags under properties, so you actually have to type that by memory, although by that point it’s intuitive (property/name/value—makes sense). The argLine tag is only needed to avoid an annoying warning about encoding.

The important part here is version 2.18.1! At the time of writing the latest version is 2.19.1. NetBeans 8.1 uses 2.10 by default for whatever reason. But if you specify 2.19.1, you get no beautiful green test results window! That is a known bug. Another known bug is that if you use an older version of TestNG (see step 1), for example, 6.8.1 that NetBeans 8.1 uses by default, you may get a “test skipped” window with exceptions in the output window saying that your transformer class can’t be loaded. It doesn’t mean that’s there’s a problem with your class! It means that those versions don’t work well. By experimenting, I found that TestNG 6.9.10 with Surefire 2.18.1 work perfectly.

Step 5. Parametrizing messages

We’re almost done. But sometimes we want our message to contain parameters. Java provides a reasonable way to do it by using MessageFormat. Let’s break our TDD thing here and start with modifying the class (or just consider it refactoring):

And the resource bundle is now

The double quotes are needed to avoid actual quoting of the {0} string, otherwise the message would be literally “The {0} parameter must not be null”, which is obviously not what we want. This is a confusing syntax, but it’s there due to historical reasons.

Now if we run our test, we find out that our “refactoring” broke something. But we didn’t break the class. We broke the test! It is now expecting the message to be exactly what is in the resource bundle. At this point we need to decide whether to test that the message looks like what it’s supposed to look like or that is exactly what it’s supposed to be. What if our test method itself is parametrized? What if we test for IllegalArgumentException and pass various invalid values? Do we need to check that the message actually contains the invalid value? We probably do, but that is, unfortunately, impossible to achieve with annotation transformers. The reason is they do just that—transform annotations. And that happens only once. So if our method is called 200 times, it will have the same annotation over and over again. So it would expect the same message. In this case, maybe it’s simpler to just do try-and-catch and check the message manually with assertEquals (and add fail in case there is no exception).

But if we need to check that the exception looks like what we expect it to look, then there is a solution. To do it right, we really should have two annotations, like @ExpectedExceptionsMessageKey and @ExpectedExceptionsMessageFormatKey. And the second one should really parse format in a manner similar to how MessageFormat.applyPattern does it. But we can probably get away with just this ugly hack for some time:

When will it break? Well, obviously when a real message, not a message format, contains something like {0} or a double quote. But, really, how often do we see those? And if it ever happens, we can easily fix it, probably by another ugly hack.

Another case when it will break is if the format is something more than just an argument number. But then again, even though we often see those in UI messages, exception messages are usually much simpler, along the lines of “Value {0} is invalid” or “Couldn”t open {0}: {1}”.

But if you feel like it, by all means, do it right! I’ve shown you a great tool, it’s up to you how to use it!