Introduction
- So I am building my own little project, which you can read about HERE and I have made the decision to use as few libraries as possible. Now that I am doing some testing I need some mock objects, which means I have to try to recreate Mockito. So this series will be me recreating Mockito(a much simpler version) the best I can. This post will be about creating a simple single use case implementation that gets our annotation working. It won't be pretty but it will work
Where and how I got started
- As you can imagine I was quite lost when first starting to recreate the annotation base library. I naively thought I could just go to the github and read the code. I call this the
bottom up approach
. However, Mockito is a very mature and complicated library and even it has poorly documented code sections, the more of the code base I looked at, the more confused I got. So I decided to start from what I know and then work backwards. I call this thetop down approach
. The only thing I really know about Mockito is the@mock
annotation and that is where we are going to start.
Annotations
- Annotations are a form of metadata, they provide data about a program that is not part of the program itself. Annotations have no direct effect on the code they annotate.
- Annotations have 3 main use cases:
1) Information for the compiler: Annotation can be used by the compiler to detect errors or supress warnings
2) Compile time and deployment processing: software tools can process annotation information to generate code.
3) Runtime processing: Some annotations are available to be examined at runtime. This is the type of annotation that we are going to create.
Declaring an annotation
- To declare an annotation type we just use the
interface
keyword and@
symbol. So create an new interface and have it look like this:
@Target({FIELD}) //field
@Retention(RetentionPolicy.RUNTIME) // runtime visibility
public @interface Gucci{
}
- It is very important that you take your time in naming this interface because this is going to be our version of Mockito's
@mock
. Mine is calledGucci
because that is my dogs name :). You have probably noticed the two other annotations, they are:
@Target({FIELD})
: This annotation marks and restricts what kind of Java elements the annotation can be applied to and for right now we only want the fields to use our annotation.
@Retention(RetentionPolicy.RUNTIME)
: This annotation specifies how the marked annotation is stored. The RetentionPolicy.RUNTIME
parameter, means that this annotation is retained by the JVM so it can be used by the runtime environment.
Getting things set up.
- Now that we have our annotation lets set everything up. To start lets create a class that we want to mock with our
@Gucci
annotation. Create a new class:
public class TestingMocks {
public String testMethodCall(){
return "We are mocking this class";
}
}
- The next lets create our test class. So go into the test folder and create a new class:
public class TestingTheExtension {
@Gucci
private TestingMocks testingMocks;
@Test
public void testingExtension(){
testingMocks.testMethodCall();
}
}
- If you have used Mockito before I am sure you have noticed that we are missing the
@ExtendWith()
annotation. That is very much intentional, because I first want to talk about JUnit a little.
Understanding @ Test
- A method annotated with
@Test
is called atest
method. A test class is a class that contains one or more test methods. For each test method JUnit create a new instance of the test class. This is done to create independence and prevent unintentional side effect in the test code. Since we only have one test method, JUnit will only create one instance of the test class.
JUnit 5 test extension model
- This extension model is really the key to everything. It is what allows us to create an instance of our annotated class. In Junit 5 the extension model revolves around events called
extension points
. There are 5 of these points in total:
1) Conditional test execution : controls whether a test should run
2) Lifecycle callback : React to event in the life cycle of a test
3) Parameter resolution : At runtime, resolves the parameter received by the test.
4) Exception handling : Defines the behaviour of a test when it encounters certain types of exceptions.
5) Test instance post processing : Executed after an instance of a test is created.(This is the execution point we are interested in)
- Ok extension points are cool but how to we take advantage of them? Good question. Well, as it turns out each
extension point
has a specific interface and the interface we are interested in isTestInstancePostProcessor
, which is the interface for theTest instance post processing extension point
Creating The single use case
- Now we can create our class that will be used by the Junit 5 extension model. Notice how it is just a simple class with the
TestInstancePostProcessor
interface. Since we implemented that interface, when Junit creates a new instance of a test class thepostProcessTestInstance
method will be called:
public class GucciExtension implements TestInstancePostProcessor{
@Override
public void postProcessTestInstance(Object o, ExtensionContext extensionContext) throws Exception {
Field [] annotatedField = o.getClass().getDeclaredFields();
for (Field field : annotatedField) {
if (field.isAnnotationPresent(Gucci.class)) {
field.setAccessible(true);
field.set(o, new TestingMocks());
};
}
}
- The
TestInstancePostProcessor
method takes two parameters:
Object o : This is the instance of the TestingTheExtenstion
class
ExtensionContext extensionContext : The current execution context. Now that definition from the documentation really doesn't mean anything to me. However, If you look at the methods that are defined on the ExtensionContext
class you will notice that they are similar to the Java class Class. So if I had to take a guess I would say that the ExtensionContext must be something similar to a normal Java Class. Which I know is weird to say, because technically when any Java type is loaded by the JVM a Class gets created. So ExtensionContext is similar to Class but the ExtensionContext gives us access to JUnit specific methods. As you can tell I am still confused on the ExtensionContext, so if anyone has a better explanation please comment down below.
- Now that we have this class we add the @ExtendsWith() annotation:
@ExtendWith(GucciExtension.class)
public class TestingTheExtenstion {
@Gucci
private TestingMocks testingMocks;
@Test
public void testingExtension(){
testingMocks.testMethodCall();
}
}
- Now just run your tests as normal and we have our very inflexible single use case that will insatiate any thing annotated with
@Gucci
into a TestingMocks class.
Conclusion
- Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
Top comments (0)