Easier Unit Tests with JNarcissus

Updated after major refactor on JNarcissus

For those that write a lot of Unit Tests in Java (yeeeeiiii, me included), i’ve just created an open source project that could interest you folks out there.

Its name is JNarcissus

Quoting Wikipedia:

in Greek mythology, Narcissus was a hunter from the territory of Thespiae in Boeotia who was renowned for his beauty. He was exceptionally proud, in that he disdained those who loved him. Nemesis saw this and attracted Narcissus to a pool where he saw his own reflection in the waters and fell in love with it, not realizing it was merely an image. Unable to leave the beauty of his reflection, Narcissus died.

JNarcissus won’t be hunting your bugs, but IT WILL fall in love with your objects. :D. Meaning that it will always notify you about things that should not happen.

Lets see an example. Lets say i have to test the following class:

package org.jnarcissus.core.sample;

public class JustAnotherTestClass {

	private String textField1;

	private String textField2;

	/**
	 * Returns the textField1 value.
	 * 
	 * @return textField1 value.
	 */
	public String getTextField1() {
		return textField1;
	}

	/**
	 * Sets the textField1 value.
	 * 
	 * @param textField1
	 *            New value.
	 */
	public void setTextField1(String textField1) {
		this.textField1 = textField1;
	}

	/**
	 * Returns the textField2 value.
	 * 
	 * @return textField2 value.
	 */
	public String getTextField2() {
		return textField1;
	}

	/**
	 * Sets the textField2 value.
	 * 
	 * @param textField2
	 *            New value.
	 */
	public void setTextField2(String textField2) {
		this.textField1 = textField2;
	}
}

Notice how the “copy-paste” demon got me tricked there (the getTextField2 and setTextField2 methods are using the wrong field). And lets face it, no matter how many year have passed, we always do copy-paste :D. If we follow the common Unit Test guideline (test just one requisite/feature per test) it would be really hard to get this bug. Lets see:

package org.jnarcissus.core.sample;

import org.junit.Assert;
import org.junit.Test;

public class JustAnotherTestClassTests {

	@Test
	public void setTextField1_validValue_getTextField1ReturnsSuppliedValue() {
		
		JustAnotherTestClass obj = new JustAnotherTestClass();
		
		Assert.assertNull(obj.getTextField1());
		
		obj.setTextField1("SomeValue");
		
		Assert.assertEquals("SomeValue", obj.getTextField1());
		
		obj.setTextField1(null);
		
		Assert.assertNull(obj.getTextField1());
	}
	
	@Test
	public void setTextField1_validValue_getTextField2ReturnsSuppliedValue() {
		
		JustAnotherTestClass obj = new JustAnotherTestClass();
		
		Assert.assertNull(obj.getTextField2());
		
		obj.setTextField2("SomeValue");
		
		Assert.assertEquals("SomeValue", obj.getTextField2());
		
		obj.setTextField2(null);
		
		Assert.assertNull(obj.getTextField2());
	}
}

Both tests will pass, because they test only one feature. And I know this is the Unit Testing philosophy, but you can also say is one of its weakness. For that (and a lot of other cases) JNarcissus was made! Lets see how we solve this problem with JNarcissus:

package org.jnarcissus.core.sample;

import org.jnarcissus.core.JNarcissus;
import org.junit.Assert;
import org.junit.Test;

public class JustAnotherTestClassTests {
	
@Test
	public void setTextField1_validValueAndAssertsWithJNarcissus_getTextField1ReturnsSuppliedValue() {

		JustAnotherTestClass obj = JNarcissus.create(JustAnotherTestClass.class);

		JNarcissus.assertNull(obj.getTextField1());
		
		JNarcissus.assertNull(obj.getTextField2());
		
		obj.setTextField1("SomeValue");

		JNarcissus.assertEquals("SomeValue", obj.getTextField1());

		obj.setTextField1(null);

		JNarcissus.assertNull(obj.getTextField1()).andPreviousAsserts();
	}

	@Test
	public void setTextField2_validValueAndAssertsWithJNarcissus_getTextField2ReturnsSuppliedValue() {

		JustAnotherTestClass obj = JNarcissus.create(JustAnotherTestClass.class);

		JNarcissus.assertNull(obj.getTextField1());
		
		JNarcissus.assertNull(obj.getTextField2());

		obj.setTextField2("SomeValue");

		JNarcissus.assertEquals("SomeValue", obj.getTextField2()).andPreviousAsserts();

		obj.setTextField2(null);

		JNarcissus.assertNull(obj.getTextField2()).andPreviousAsserts();
	}
}

Executing the code now throws the following error:

java.lang.AssertionError: Called: org.jnarcissus.core.sample.JustAnotherTestClass.getTextField1()
Expected: null
Actual: “SomeValue”

NICE! Now the tests fail!!! Aaaaa the red color… i don’t know if i love more making tests fail or making tests pass! 🙂 But how does the magic happens? Well, the first important line is this:

JustAnotherTestClass obj = JNarcissus.create(JustAnotherTestClass.class);

This will create an instance of the supplied class but, this instance will be special. It will have all its methods calls monitored by JNarcissus. But JNarcissus does not know yet what to monitor. Normally on a test, we ASSERT what we expect to be true (A value equals to an expected value, a condition returning false, etc). Look at the following line:

JNarcissus.assertNull(obj.getTextField1());

With the line above, we are providing the information JNarcissus needs to monitor our instance. It means something like this:

JNarcissus, assert that the obj.getTextField1() method returns null and, EXCEPT I SAY OTHERWISE, all subsequent calls to the obj.getTextField1() method should return null. ALWAYS.

As our test continues, we may need to update the information to be monitored. Look at the next two lines below:

JNarcissus.assertNull(obj.getTextField2());
obj.setTextField2("SomeValue");

The first line only makes the same as the previous assert but now to the obj.getTextField2() method. But the second line is calling the obj.setTextFielld2 method with a value. This means that the expected return value from the obj.getTextField2() method should NOT BE NULL anymore. It should be equal to the one supplied to the obj.setTextFielld2 method. We need to update this information on JNarcissus (Remember the “EXCEPT I SAY OTHERWISE” condition?). We do this simply calling another assert on the same method:

JNarcissus.assertEquals("SomeValue", obj.getTextField2()).andPreviousAsserts();

To JNarcissus, this means something like this:

JNarcissus, assert that the obj.getTextField2() method returns a value equals to “SomeValue” and, EXCEPT I SAY OTHERWISE, all subsequent calls to the obj.getTextField2() method should return a value equals to “SomeValue”. ALWAYS. And also, execute all the previous asserts i made until now.

The .andPreviousAsserts() method executes all the asserts the code made before. And of course, it will only execute the most recent assert for a given method. I like to call this feature Assertive Memory. Because basically, is a memory of all the current asserts made to an object. If we were to look at the assertive memory state through the test, it would look like this:

obj.getTextField1() obj.getTextField2()
Before setting value Assert is null Assert is null
After setting value Assert is null Assert is equals to “SomeValue”

When you are updating the information (lets read it, the assert you want to keep stored) about a method, JNarcissus does not only take in consideration the method witch you are calling, but also the arguments you use. For example, lets say we have a method that accepts an argument, and that we want to monitor different return values from it:

	
/**
* A method with an argument that impacts the returned value.
* 
* @param number
*            Number argument.
* @return The supplied number to String.
*/
public String numberToString(int number) {

if (number == 0)
	return "Zero";
else if (number == 1)
        return "One";
else
	return "Don't know";
}

We can monitor methods with arguments the same way we do with methods without arguments:

	
@Test
	public void numberToString_returnsCorrectValue() {
		
		JustAnotherTestClass obj = JNarcissus.create(JustAnotherTestClass.class);
		
		JNarcissus.assertEquals("Zero", obj.numberToString(0));
		JNarcissus.assertEquals("One", obj.numberToString(1));
		JNarcissus.assertEquals("Don'tknow", obj.numberToString(2));
	}

What this means to JNarcissus:

JNarcissus, assert that the obj.numberToString method called with the argument 0 returns a value equals to “Zero” and, EXCEPT I SAY OTHERWISE, all subsequent calls to the obj.numberToString method called with the argument 0 should return a value equals to “Zero”. ALWAYS.
JNarcissus, assert that the obj.numberToString method called with the argument 1 returns a value equals to “One” and, EXCEPT I SAY OTHERWISE, all subsequent calls to the obj.numberToString method called with the argument 1 should return a value equals to “One”. ALWAYS.
JNarcissus, assert that the obj.numberToString method called with the argument 2 returns a value equals to “Don’tKnow” and, EXCEPT I SAY OTHERWISE, all subsequent calls to the obj.numberToString method called with the argument 2 should return a value equals to “Don’tKnow”. ALWAYS.

Executing this test will throw the error:

java.lang.AssertionError: org.jnarcissus.core.sample.JustAnotherTestClass.numberToString(2)
Expected: “Don’tknow”
Actual: “Don’t know”

I’ve made this error on purpose, just to show that JNarcissus monitors method call with arguments also. We can easily fix the test by correct the string “Don’tknow” to “Don’t know”.

Why all the trouble

Well, i wrote JNarcissus because in some point while doing Unit Tests, I noticed that in some more complicated cases, I was doing the same asserts very often. JNarcissus main reason to exist was to erase du/tri/quadru/####plicated “asserts” code. The other main reason was to understand how Mocking frameworks are made :D. You see, under the hood, JNarcissus uses the same technique most of the Mocking Frameworks use, that is: In runtime, generate a class that extends the class you want to mock, and direct all method calls made to that instance to the mocking framework stubs. In fact, the first version of JNarcissus was made based on the GREAT AND AWESOME (I really, really like it) mocking framework Mockito. You can see the influence from Mocking frameworks, since in JNarcissus you can actually assert information about the monitored methods:

JNarcissus.assertMethod(obj, new CallCountMatcher(1)).numberToString(0);
JNarcissus.assertMethod(obj, new CallCountMatcher(new GreaterOrEqual<Integer>(1))).numberToString(0);

The first line above will assert that the number of calls made to the obj.numberToString(0) method was exactly 1. The second line shows just an overload to the CallCountMatcher constructor that receives a Matcher object (yes, from the awesome library Hamcrest). that asserts that the number of calls to the obj.numberToString(0) method is Greater or Equal to 1. This is a good feature when you use dependency injection on your tests, and does not want to create a mock, but simply verify that a method was called.

Internally, JNarcissus uses the Hamcrest matchers to do the asserts. So you can also do something like this:

// These two lines means the same thing
JNarcissus.assertNull(obj.getTextField1());
JNarcissus.assertThat(obj.getTextField1(), CoreMatchers.nullValue());

Whats next?

Well, JNarcissus it still in its beggining, but i already use it for making Unit Tests at my projects. I plan to port JNarcissus to .NET also, but still don’t know when it will be. Like most of the mocking and dependency injection frameworks that use the “derived class technique” or “proxy class technique”, JNarcissus has some limitations. It cannot monitor final methods or classes. I will try to overcome this limitation in the future, but only if there is the demand to.

Advertisements

2 comments so far

  1. szczepiq on

    I’m glad you’ve taken on writing test tools – the more choice we have the more tests are written… well… at least I hope so 🙂

    I’d like to see a more compelling use case for using JNarcissus. The problem with the use case you provided is:

    – it is showing how to test getters and setters. Normally I don’t test getters and setters because they are tested implicitly anyway via other tests and they have too little complexity to bother (usually they are generated or even ‘hidden’ like in groovy).

    – I could achieve the same level of validation with simple & easy state based testing:

    obj.setTextField1(“one”);
    obj.setTextField2(“two”);

    Assert.assertEquals(“one”, obj.getTextField1());
    Assert.assertEquals(“two”, obj.getTextField2());

    • Rafael Ribeiro on

      Thanks for the encouragement. I know that if we have TOO MANY choices, it gets harder to decide for the one that its best or more correct. There is a nice video on TED here that talks about this :D.

      I know that currently there is no mainstream tool that takes this approach on Unit Tests. So at least i’m not causing more confusion on the choices hehehe.

      You are correct, you can implement the same test case without JNarcissus, the problem is that if you don’t have experience in Unit Testing, you can easily be tricked by them. And even if you have experience, sometimes you just get tricked anyway. Humans will always have bad days, so we cannot expect to be perfect all the time.

      My main objective is to make testing easier. Note that with the sample you supplied, if i just change the order of the lines, the test will not get the bug:

      obj.setTextField1(“one”);
      Assert.assertEquals(“one”, obj.getTextField1());
      
      obj.setTextField2(“two”);
      Assert.assertEquals(“two”, obj.getTextField2());
      

      This happens because Asserts usually are not persistent. My objective was to supply a “safe-net” for those cases when we write the Unit Test wrong. And if we want to get more people doing tests, the easier it gets, the more people will do it.

      I will work on more compelling use cases. Thanks for the comment.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: