views:

198

answers:

2
+4  A: 

When you write a unit test, you try to test if (in this case) the method does what it is supposed to do. You should not look at the implementation and write your test from that. Instead, you should think about what inputs the method should be able to handle, and what should be the result (returned value and/or side effects) after the method has been called.

Then you should write one or more tests that calls the method with valid and and invalid inputs and make the test confirm that the results matched what you thought would happen.

This was a short and incomplete description, read more at Wikipedia and junit.org.

Here is an old (2005) but working guide to JUnit in Eclipse.

Peter Jaric
Thanks for this.I'm using Cobertura tool to calculate the line and branch coverage and the line numbers were the indications where the code is was not tested. Also, the private accessor/visibility for the method was a mistaken and its been changed to public now.Could somebody give me a sample test case for this piece of code. Would be much appreciated.Thanks
Global Dictator
Do you mean some skeleton code where you can fill in the specifics? Because, as I said, you can not write a test if you don't know what behavior it should test.
Peter Jaric
I strongly disagree. You describe black box testing, while unit testing is white-box testing - you _do_ see the code to be tested, as it is (ideally) your own code! It is another matter that you should think about what contract the method is supposed to fulfill, and write test cases to verify that.
Péter Török
And let me add this: don't write a test *just* to increase your coverage if the test doesn't really test your code. That would be false test coverage indeed.
Peter Jaric
@Péter Török: If you wrote the test yourself, that is true of course. But I am quite sure we at SO didn't write the code above, and hence we could only write tests that confirm that the code is working if we get no indication of its purpose. And consider test driven development, where unit testing is at the core. There you don't even have the code when you write the test.
Peter Jaric
@Péter Török - I know where you're coming from, but you should definitely *start* black-box. The goal with a unit test after all is often not so much to ensure that the code works **right now**, but rather to have a test in place to check that future, modified versions of the code will *continue* working. If you use your white-box view to say "oh, I know this can never be a problem because of the implementation details here so I won't test it" then what happens when someone refactors the class?
Andrzej Doyle
@Andrzej, @Peter: I see your point, and I agree that one should not test _only_ what the code explicitly does right now. That's why I mentioned testing against contract above. And it is true that in pure TDD one writes test first, then the code. However, in testing legacy code (as I believe is the case in this question), you can't avoid the fact that the code is already written, and often, lacking any useful documentation, you must deduce the supposed "contract" from the code itself.
Péter Török
+6  A: 

I recommend using a mocking framework, such as EasyMock. Mocks allow you to configure dependencies with the desired behaviour for your tests. In your case, you need a mock DocumentEvent and ideally another one for component, which I guess is a class member.

The two aspects to unit testing

  • how to test, i.;e. the technical details of assembling the right set of objects in the right state required for the test to run properly (aka the _test fixture), and
  • what to test, i.e. the scenarios to validate.

How to test

Eclipse supports JUnit out of the box, so you may quickly generate new JUnit testcases (in Project Explorer context menu: New -> (Other ->) JUnit -> JUnit Test Case), then run it by clicking on the Run button.

Setting up the test fixture in your case would look something like this, using EasyMock (and assuming you can pass the component as a constructor parameter to your tested class):

private Component mockComponent;
private ClassUnderTest classUnderTest;
private DocumentEvent mockEvent;
private Document mockDocument;
private Highlighter mockHighlighter;

@Before
public void setUp() {
    mockComponent = createMock(Component.class);
    classUnderTest = new ClassUnderTest(mockComponent);
    mockEvent = createMock(DocumentEvent.class);
    mockDocument = createMock(Document.class);
    expect(mockEvent.getDocument()).andStubReturn(mockDocument);
    expect(mockDocument.getLength()).andReturn(1);
    mockHighlighter = createMock(Highlighter.class);
    expect(mockComponent.getHighlighter()).andReturn(mockHighlighter);
}

@Test
public void testEmptyText() {
    expect(mockDocument.getText(0, 1)).andStubReturn("");
    mockHighlighter.removeAllHighlights();
    replay(mockComponent, mockEvent, mockDocument, mockHighlighter);

    classUnderTest.handle(mockEvent);

    verify(mockComponent, mockEvent, mockDocument, mockHighlighter);
}

This test assumes that maxMessageSize is at least 1 by default - setting maxMessageSize up for the test is left to you as an exercise as the code snippet you published gives no clue for that.

What to test

The method you show gets text from the document associated with the event, then based on its length, it does different things. I would write at least the following unit tests for this:

  • empty document text with maxMessageSize == 0
  • empty document text with maxMessageSize > 0
  • nonempty document text with maxMessageSize == text.length()
  • nonempty document text with maxMessageSize > text.length()
  • nonempty document text with maxMessageSize < text.length() and addHighlight() throwing BadLocationException

Notes

  1. sensing the BadLocationException is a bit tricky, since all it produces is an output to stdout; luckily, you can easily reassign stdout via System.setOut. However, you may want to consider improving exception handling, at least by using a logging framework instead of printing to stdout.
  2. from the code it seems that other methods (such as removeAllHighlights() and/or getText()) may also throw BadLocationException, however the try-catch blocks are not well organized. I would consider adding more unit tests where those methods throw, and after that, refactoring the exception handling code.

Update

I thought there was something wrong that I was doing...Can you please provide me with the modified/corrected code please???

Your testSetLength method is not really testing anything - you need assert statements (and/or EasyMock verification) in order for your unit tests to actually verify some behaviour. However, it provides the missing clue for setting up the tested class. So I try to unify your two test methods to create one which is hopefully working (I am writing from memory, so I can't guarantee it will all compile and run perfectly at first try) :

  @Test 
  public void testEmptyText() { 
    // set up the test class with a specific max length
    classUnderTest = new MaxLength(125); 
    // this shall be called from inside decorate()
    mockDocument.addDocumentListener(classUnderTest); 
    // the mock document shall always return an empty text
    EasyMock.expect(mockDocument.getText(0, 1)).andStubReturn(""); 
    // we expect this to be called from inside handle()
    mockHighlighter.removeAllHighlights();
    // start replay mode
    EasyMock.replay(mockComponent, mockEvent, mockDocument, mockHighlighter); 
    // inject mock component into tested object
    maxListener.decorate(mockComponent); 

    // call the tested method
    classUnderTest.handle(mockEvent); 

    // verify that all expected calls to the mocks have been made    
    EasyMock.verify(mockComponent, mockEvent, mockDocument, mockHighlighter); 
  }
Péter Török
Hi Péter Török,Could you please give me some instructions to install EasyMock manually. I tried adding this dependency in the POM file as instructed at the Easymock documentation page and then tried running mvn instal but it didnt work. Was I doing something wrong there?? In which case could you let me know how to get this dependecny from teh maven repo..Also, I've downloaded the jar file and was wondering how to install Easymock that way.Thanks.
Global Dictator
@Global, strange. For us it worked. What do you precisely mean by "mvn instal didnt work"?
Péter Török
I added the following code to the project POM file at top level: <dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> <version>3.0</version> <scope>test</scope> </dependency>There were no further instructions on the EasyMock documentation, hence I tried downloading the dependency from the Maven repo by running this command.. I've just started using Maven couple of weeks ago, so didnt know what else I needed to do.Thx.
Global Dictator
@Global, by "top level" do you mean that it is not inside the `<dependencies>` section? If so, move it there and retry `mvn install`.
Péter Török
@Péter No, I did include the easymock in the <dependencies> section. How can we check that this dependency is actually present and available to us for download?
Global Dictator
Also, I'm trying to imported the EasyMock by having this statement import org.easymock.EasyMock.*;but when using your method (createMock()) is throwing an error..
Global Dictator
@Global, You can `import static org.easymock.EasyMock.*` to make the code work as it is, or you can add the `EasyMock.` qualifier to the individual calls. Sorry, I should have mentioned that in the answer :-(
Péter Török
@Global, for verifying the dependency, use the [maven-dependency-plugin](http://maven.apache.org/plugins/maven-dependency-plugin/). I don't have much experience with it but try `mvn dependency:resolve -Dsilent=true`.
Péter Török
@Peter, do you mean that if I add this import statement, I dont have to add EasyMock in the POM file. I'm sorry but i'm really confused and totally lost as to what needs to be done to get this to work. Also, what are the prerequisites to make this import statement to work? Do I need to play with the jars or something?? Please advise...
Global Dictator
Finally got EasyMock working. Just used EasyMock.createMock() with the import statement at the top. Looking at your code, could you please briefly explain what each statement in the code is doing. I know little about what expect(), Verify(), replay() methods do but what about andStubReturn() and return() method??Thanks
Global Dictator
@Global, glad to hear :-) `expect(mockDocument.getLength()).andReturn(1)` makes the mock document expect a single call to `getLength()`, and return 1 from that call. `andStubReturn()` makes the mock return the specified value from any number of subsequent calls to that method. See the section _Specifying Return Values_ in [the EasyMock documentation](http://easymock.org/EasyMock3_0_Documentation.html) for more details.
Péter Török
@Peter: For some reason when I running the test case, I'm not getting any coverage at all. Am I doing something wrong here?? Further researching suggests that I need to use test.perform() method to call the method I want to test.. Is that correct?? Can you please suggest soemthing?? Here is the code:
Global Dictator
public class TestMaxLength{ static final int maxMessageSize = 125; JTextPane textPane = new JTextPane(); //***EasyMock varibles**** private JTextComponent mockComponent; private MaxLength classUnderTest; private DocumentEvent mockEvent; private Document mockDocument; private Highlighter mockHighlighter;
Global Dictator
@Before public void setUp() { mockComponent = EasyMock.createMock(JTextComponent.class); mockEvent = EasyMock.createMock(DocumentEvent.class); mockDocument = EasyMock.createMock(Document.class); EasyMock.expect(mockEvent.getDocument()).andStubReturn(mockDocument); EasyMock.expect(mockDocument.getLength()).andReturn(256); mockHighlighter = EasyMock.createMock(Highlighter.class); EasyMock.expect(mockComponent.getHighlighter()).andReturn(mockHighlighter); }
Global Dictator
@Test public void testSetLength() { MaxLength maxListener = new MaxLength(125); maxListener.decorate(textPane); }}
Global Dictator
The decorate(JtextComponent jComponent) method is present in the class to be tested (MaxLength) and is defines as :public final void decorate(final JTextComponent c) { //TODO throw exception if already decorating this.component = c; component.getDocument().addDocumentListener(this); }
Global Dictator
@Global, I added the content of your comments as an update to your original post - please do so in the future, as code is very badly readable in comments :-) Do I understand correctly that your unit tests actually run, and you get the green bar meaning all tests pass, but you get no coverage output?
Péter Török
@Global, I think the coverage configuration is distinct from the unit test configuration, but I have never used coverage tools in Eclipse so I can't give you much concrete help with this. Have you rebuilt your project after switching Cobertura on? IMHO the coverage tool (if configured properly) should work fine with vanilla JUnit test methods. I have never heard about that `test.perform()` method and I doubt it would have anything to do with your coverage setup - could you give a reference to the source of this information?
Péter Török
@Global, the unit test `testSetLength` does not seem to test the method `handle` you showed in your original post. So the mock setup is irrelevant for this test. And even if it were, you don't call `EasyMock.replay` so the mocks are still in recording mode during the whole test.
Péter Török
@Peter, I thought there was something wrong that I was doing...Can you please provide me with the modified/corrected code please???
Global Dictator
@Peter, I forgot to mention that I separated the test methods into 2 methods, one that uses the mock objects and other without any mock objects. I have now added both the methods in the code above..Cld you have a look and let me know what am I doing wrong..
Global Dictator
@Global, see my update.
Péter Török
Hi Peter, thanks for this. But I'm getting a java.lang.NoClassDefFoundError when trying to create a mock Component class. mockComponent = EasyMock.createMock(Component.class); Rest all the createMock statements are working fine. What do you suggest could be causing this!!!Thanks
Global Dictator
@Global, your code above contains `mockComponent = EasyMock.createMock(JTextComponent.class)`, isn't it working? In my first example, `Component` was just a placeholder name for a class whose real name I didn't know - sorry for not making this clear. I assume from your second code sample that the real type is `JTextComponent`.
Péter Török
@Peter: Yes, thats right that I'm using JTextComponent but am not able to create the mock object of it.. Its giving me java.lang.NoClassDefFoundError when running the Junit test. Could you please help!!!
Global Dictator
@Global, sounds like a classpath problem. Make sure that your test classes also "see" the jar containing JTextComponent.class at both compile and runtime.
Péter Török
@Peter: How can I achieve to make the test classes see the JTextComponent class?? I thought that all Swing text components inherit from the same superclass, JTextComponent,which is made available as part of the java.Swing package.... Please advise..
Global Dictator
@Peter: Could you please advise what could I do here o fix this problem...Thanks
Global Dictator
@ Peter: Pleas see the update in my original question. Wld appreciate your comments..
Global Dictator
@Global, the error message `Expectation failure on verify: getHighlighter(): expected: 1, actual: 0` means that the method `getHighlighter()` was expected to be called once (as was configured in the last statement of your `setUp()` method), but it was not called. Which is strange because in the `handle` method you showed above, `component.getHighlighter()` is supposed to be called always (except if an exception was thrown within the method). I suggest you debug the test to figure out what is actually happening inside `handle`.
Péter Török
@Global, hope that helps. Note that I won't be present on SO very often in the coming weeks, so don't be disappointed if you don't get quick answers from me...
Péter Török
@Peter: It was really helpful. Appreciate that!!One thing that I'm not sure is that if mockComponent.getHighlighter() is called in handle() then, so I need to add expect for method getHighlighter() on mockComponent too???
Global Dictator
@Global, it's already there in the `setUp` method :-)
Péter Török
Thanks, all working fine now.
Global Dictator