xUnit
Lets start with one of the most common examples people in test would be exposed to:[Test] //Java: @Test() public void TestSomeFeature() { /*... */}
First to be clear, the attribute (or commented out annotation) is on the first line. For clarities sake, for the rest of the article I'm going to say "annotation" to mean either attribute or annotation. It defines that the method is in fact a "Test" which can be run by xUnit (NUnit, JUnit, TestNG).
The annotation does not in fact change anything to do with the method. In fact, in theory xUnit could run ANY and ALL methods, including private methods in an entire binary blob (jar, dll, etc.) using reflections. In order to filter out the framework and helper methods, they use annotations, requiring them to be attached so they know which methods will run. This by itself is a very useful feature.
The Past Anew
Looking back at my example from my previous post, you can now see a few subtle changes which I have marked NEW:class HighScore { String playerName; int score; @Exclude()//NEW Date created; int placement; String gameName; @CreateWith(Class = LevelNamer.class)//NEW String levelName; //...Who knows what else might belong here. }
Again, the processing function could be subtly modified to support this change. The change is again marked NEW:
function testVariableSetup(Object class) { for each variable in class.Variables { if(variable.containsAnnotation(Exclude.class)) //NEW continue;//NEW don't process the variable. if(variable.containsAnnotation(CreateWith.class)) { //NEW variable.value = CreateNewInstance(variable.getAnnotation(CreateWith).Class).Value();//NEW ; Create New Instance must return back the interface. }//NEW if(variable.type == String) then variable.value = RandomString(); if(variable.type == Number) then variable.value = RandomNumber(); if(variable.type == Date) then variable.value = RandomDate(); }
So what this code demonstrates is the ability to exclude fields in any given class by attaching some meta data to it, which any functionality can look at but doesn't have to. In the high score class we marked the created date variable as something we didn't want to set, maybe because the constructor sets the date and we want to check that first. The second thing we did was we set a class to create the levelName. The levelName might have a custom requirement that it follow a certain format. Having a random String would not do for this, so we created an annotation that takes in a class which will generate the value.
Now we could have a different custom annotation for each and every custom value type, but that would defeat the purpose of make this as generic as possible. Instead, we use a defined pattern which could apply to several different variables. For example, gameName also had to follow a pattern, but it was different from the levelName pattern. You could create another class called gameNamer and as long as it followed the same design (had a method called "Value()" that returned a string), you could just use the CreateWith(Class=X) annotation and they would act the same. This means you would not need to add another case in the testVariableSetup method or even change it. In Java and C# the mechanism for this is a common ancestor which can be either an interface or an abstract class. That is to say, they both inherit from the same abstract class or implement the same interface. For the sake of completeness and to help make this make sense, I have included an updated pseudo code examples below:
class HighScore { String playerName; int score; @Exclude()//NEW Date created; int placement; @CreateWith(Class = GameNamer.class)//NEW String gameName; @CreateWith(Class = LevelNamer.class)//NEW String levelName; //...Who knows what else might belong here. } // All NEW below: class ICreateWith { String Value(); } class GameNamer implements ICreateWith { String Value() { return "Game # " + RandomNumber(); } class LevelNamer implements ICreateWith { String Value() { return "Level # " + RandomNumber(); } //In some class class some { ICreateWith CreateNewInstance(Class class) { return (ICreateWith)class.new(); } }
TestNG - Complex but powerful
One last example that is a little more of a real life example that is common in the testing world. Although I think the code might be a little too complex to get into here, I want to talk about a real life design and how it works in general. TestNG uses annotations with an interesting twist. Say you have a "Group", a label saying that this is part of a set of tests. Well, perhaps you have a Group called "smoke" for the smoke tests you want to run separate from all the others. TestNG might support filtering, but between TestNG and Maven and ... you decide you want to do determine the filtering of tests at run time using a flag somewhere (database, environment variable, wherever) that says "run smoke only". During run time, TestNG calls an event saying "I'm about to run test X, here is all the annotation data about it. Would you like to change any of it?" At this point you can read the Group information about the test. If your flag says smoke only, you can then check the groups the test has. If the Group list does not have smoke in it, you set the test to enabled=false, changing the annotation's data at run time. TestNG calls this Annotation Transformations. I call it cool.The weird part is that you are modifying data at run time annotation data that is hard coded at compile time. That is to say, annotation values cannot be actually changed at runtime*, but a copy of the instance of them can be. That is what TestNG actually changes from what I can tell.
If you are reading this and saying, this topic is rather confusing, don't feel too badly. I know it is confusing. The TestNG part in particular is a bit mind bending. And to be clear, I don't see myself as an expert. There are way more complex ideas out there that just amaze me.
* This is from what I can tell. Perhaps there are reflective properties to let you do this. You can however override annotations through inheritance, but that is a more complex piece.
No comments:
Post a Comment