Mocked Observable Observation - a java library to simulate sending of events (via java listeners) from a mocked collaborator to a tested object.
When writing a lot of unit tests for large Java enterprise applications, certain problems tend to occur repeatedly:
- Simulating the sending of events (via Java listeners) from mocked collaborators to tested objects.
- Verifying the complete deregistration of listeners registered by mocked collaborators.
And all of these needs to be done without modifying production code.
While this process can be done manually, Mockobor makes it simpler, faster, and requires fewer implementation details in your tests.
Mockobor depends only on the following libraries:
- eclipse non-null annotations (org.eclipse.jdt.annotation)
To use Mockobor in unit tests:
- Ensure you are using Java 11 or more recent version.
- Include at least one of the following mocking tools in your tests:
- Mockito 5.0.0+
- EasyMock 3.4+
- Other mocking tools can also be added — refer to Using unsupported mocking tools.
if you use java 8 or Mockito 2.20.1 - 4.11.0, you can still use Mockobor 1.0.5 (Maven Central)
Given that you have the following classes in your Java application:
- Your class under test, which adds (for example, in the constructor) a listener to a specified observable object and removes it in another method
(for example, in
destroy()
):
/** Object that you want to test. */
public class TestedObserver {
private final SomeObservable someObservable;
private final Observer observer = new ObserverIml();
private final PropertyChangeListener propertyChangeListener = new PropertyChangeListenerImpl();
private final MyListener myListener = new MyListenerImpl();
private final MyAnotherListener myAnotherListener = new MyAnotherListenerImpl();
/** It registers some listeners by the specified (in tests - mocked) observable object. */
public TestedObserver( SomeObservable someObservable ) {
this.someObservable = someObservable;
someObservable.addPropertyChangeListener( "prop", propertyChangeListener );
someObservable.addMyListener( "sel", myListener );
someObservable.addTwoListeners( myListener, myAnotherListener );
someObservable.addObserver( observer );
}
/** And removes all listeners on destroy. */
public void destroy() {
someObservable.deleteObserver( observer );
someObservable.removeTwoListeners( myListener, myAnotherListener );
someObservable.removeMyListener( "sel", myListener );
someObservable.removePropertyChangeListener( "prop", propertyChangeListener );
}
}
Note: It is not strictly necessary to register listeners by directly invoking addXxxListener
methods. Registration can be performed using annotations,
aspects, or other mechanisms. What
is important is that the registration methods of the observable object are invoked somewhere.
- A collaborator of the tested object that fires events to the registered listeners:
/** Some observable object with ability to register listeners/observers. */
public interface SomeObservable {
// property change support
void addPropertyChangeListener( String propertyName, PropertyChangeListener listener );
void removePropertyChangeListener( String propertyName, PropertyChangeListener listener );
// typical java listeners
void addMyListener( String selector, MyListener myAnotherListener );
void removeMyListener( String selector, MyListener myAnotherListener );
// another typical java listeners
void addTwoListeners( MyListener myListener, MyAnotherListener myAnotherListener );
void removeTwoListeners( MyListener myListener, MyAnotherListener myAnotherListener );
// Observable
void addObserver( Observer o );
void deleteObserver( Observer o );
}
- and listeners:
public interface MyListener {
void somethingChanged1( Object somethingNewValue );
int somethingChanged2( Object somethingNewValue );
}
public interface MyAnotherListener {
void somethingOtherChanged( Object somethingOtherValue );
}
In tests, we mock the collaborator (SomeObservable
) using one of the supported mocking tools
(see Dependencies) and create a notifier object (ListenersNotifier
) to send events:
class TestedObserver_Test {
// create mock of SomeObservable
private final SomeObservable mockedObservable = Mockito.mock( SomeObservable.class );
// create notifier for SomeObservable
private final ListenersNotifier notifier = Mockobor.createNotifierFor( mockedObservable );
// Object under tested. It registers listeners by the specified SomeObservable object.
private final TestedObserver testObject = new TestedObserver( mockedObservable );
...
}
As you can see, SomeObservable
uses (among other things) typical Java-style listeners, such as MyListener
, with registration methods like addMyListener
and removeMyListener
. Mockobor
treats such classes as "using a typical Java listener" and creates a base notifier—ListenersNotifier
—to send events.
Now, to simulate processes in SomeObservable
, we will send events to the listeners MyListener
and MyAnotherListener
, which are registered in the
constructor of TestedObserver
:
class TestedObserver_Test {
private final SomeObservable mockedObservable = Mockito.mock( SomeObservable.class );
private final ListenersNotifier notifier = Mockobor.createNotifierFor( mockedObservable );
private final TestedObserver testObject = new TestedObserver( mockedObservable );
@Test
void testSendEventToJavaStyleListeners() {
// Send events to testObject using listener interfaces (first method):
( (MyListener) notifier ).somethingChanged1( newValue );
int answer1 = ( (MyListener) notifier ).somethingChanged2( newValue2 );
( (MyAnotherListener) notifier ).somethingOtherChanged( newValue3 );
// Send events to testObject using ListenersNotifier (another approach, identical to the above):
notifier.notifierFor( MyListener.class ).somethingChanged1( newValue );
int answer2 = notifier.notifierFor( MyListener.class ).somethingChanged2( newValue2 );
notifier.notifierFor( MyAnotherListener.class ).somethingOtherChanged( newValue3 );
// If listeners are registered with a specific non-empty qualifier
// (e.g., in the `TestedObserver` constructor: someObservable.addMyListener("sel", myListener)),
// you can then send events to those listeners:
notifier.notifierFor( "sel", MyListener.class ).somethingChanged1( newValue );
notifier.notifierFor( MyListener.class, selector( "sel" ) ).somethingChanged1( newValue ); // exactly as above
// To notify (send the same event to) listeners of the same class
// that are registered with at least one of the specified selectors
// (in this case: either without a selector or with "sel" as the selector):
notifier.notifierFor( MyListener.class, selector(), selector( "sel" ) ).somethingChanged1( newValue );
}
}
See also UsageExample_TypicalJavaListener_Test.java
If your collaborator (observable object) provides methods such as
void addPropertyChangeListener(PropertyChangeListener listener)
or
void addPropertyChangeListener(String propertyName, PropertyChangeListener listener)
,
it is treated as "similar to PropertyChangeSupport."
In such cases, Mockobor creates a specialized notifier called PropertyChangeNotifier
:
class TestedObserver_Test {
private final SomeObservable mockedObservable = Mockito.mock( SomeObservable.class );
private final ListenersNotifier notifier = Mockobor.createNotifierFor( mockedObservable );
private final TestedObserver testObject = new TestedObserver( mockedObservable );
@Test
void testSendEventToPropertyChangeListeners() {
// using PropertyChangeNotifier
PropertyChangeNotifier propertyChangeNotifier = (PropertyChangeNotifier) notifier;
propertyChangeNotifier.firePropertyChange( null, "o2", "n2" );
propertyChangeNotifier.firePropertyChange( "prop", "o1", "n1" );
// using ListenersNotifier
notifier.notifierFor( PropertyChangeListener.class )
.propertyChange( new PropertyChangeEvent( mockedObservable, "p4", "o4", "n4" ) );
// Using `ListenersNotifier` with selectors
// behaves exactly the same as:
// - `propertyChangeNotifier.firePropertyChange(null, 'o5', 'n5')`
// - `propertyChangeNotifier.firePropertyChange('prop', 'o5', 'n5')`
notifier.notifierFor( PropertyChangeListener.class, selector(), selector( "prop" ) )
.propertyChange( new PropertyChangeEvent( mockedObservable, "prop", "o5", "n5" ) );
// direct using listener interface (PropertyChangeListener)
( (PropertyChangeListener) notifier )
.propertyChange( new PropertyChangeEvent( mockedObservable, "prop", "o3", "n3" ) );
}
}
See also UsageExample_BeanPropertyChange_Test.java
If your collaborator (observable object) provides a method such as void addObserver(Observer observer)
, it is treated as "similar to Observable."
In such cases, Mockobor creates a specialized notifier called ObservableNotifier
:
class TestedObserver_Test {
private final SomeObservable mockedObservable = Mockito.mock( SomeObservable.class );
private final ListenersNotifier notifier = Mockobor.createNotifierFor( mockedObservable );
private final TestedObserver testObject = new TestedObserver( mockedObservable );
@Test
void testSendEventToPropertyChangeListeners() {
// using ObservableNotifier
ObservableNotifier observableNotifier = (ObservableNotifier) notifier;
observableNotifier.notifyObservers();
observableNotifier.notifyObservers( "v1" );
// using ListenersNotifier
notifier.notifierFor( Observer.class ).update( null, "v3" );
// direct using listener interface (Observer)
( (Observer) notifier ).update( null, "v2" );
}
}
See also UsageExample_Observable_Test.java
You can use Mockobor to verify whether all listeners registered by the mocked observable object have been unregistered:
class TestedObserver_Test {
private final SomeObservable mockedObservable = Mockito.mock( SomeObservable.class );
private final ListenersNotifier notifier = Mockobor.createNotifierFor( mockedObservable );
private final TestedObserver testObject = new TestedObserver( mockedObservable );
@Test
void testAllListenersAreRemoved() {
// tested object should remove itself from the specified PropertyChangeSupport object on close.
testObject.destroy(); // or close() or dispose() etc.
// check that all listeners are unregistered
Mockobor.assertThatAllListenersAreUnregistered( notifier );
}
}
See also UsageExample_allListenersAreUnregistered_Test.java
If you are using Mockito as your mocking tool, you can seamlessly combine Mockito annotations with Mockobor:
@Mock
private SomeObservable mockedObservable;
@InjectMocks
private AnnotationsTestObject testedObserver;
private ListenersNotifier notifier;
@Test
void test_notifications() {
...
notifie = Mockobor.createNotifierFor( mockedObservable );
...
notifier.notifierFor( MyListener.class ).onChange( 1f );
...
}
See UsageExample_MockitoAnnotation_Test.java
To simulate event sending (via Java listeners) from a mocked collaborator to the tested object, Mockobor generates a special notifier object for the mocked observable:
ListenersNotifier notifier = Mockobor.createNotifierFor( mockedObservableObject )
Note: Your test object must listen to the same instance of the mocked observable object specified in the Mockobor.createNotifierFor
invocation.
The notifier object implements the following interfaces, depending on the methods found in the specified mocked observable object:
ListenerNotifier
– always implements.XxxListener
(standard Java-style listener) – implements if methods likeaddXxxListener(XxxListener)
are detected.PropertyChangeNotifier
+PropertyChangeListener
– implements if methods likeaddPropertyChangeListener(PropertyChangeListener)
are detected.ObservableNotifier
+Observer
– implements if methods likeaddObserver(Observer)
are detected.
This notifier object can be utilized to perform the following actions:
-
Send events to the test object:
- Using the
notifierFor
method:notifier.notifierFor(XxxListener.class).<listener method>(arguments)
- Directly via the listener interface:
((XxxListener) notifier).<listener method>(arguments)
- Using
PropertyChangeNotifier
(when applicable):((PropertyChangeNotifier) notifier).firePropertyChange(...)
- Using
ObservableNotifier
(when applicable):((ObservableNotifier) notifier).notifyObservers(...)
- Using the
-
Verify complete deregistration of listeners:
- For a single notifier:
assertThat(notifier.allListenersAreUnregistered()).isTrue()
- For multiple notifiers:
Mockobor.assertThatAllListenersAreUnregistered(notifier1, ..., notifierN)
- For a single notifier:
For more details see JavaDoc (Mockobor, Mockobor.createNotifierFor , Mockobor.assertThatAllListenersAreUnregistered, ListenerNotifier) and Examples.
Sometimes, a listener should or can be registered not for all events, but only for specific qualified events.
For example, in java.beans.PropertyChangeSupport.addPropertyChangeListener(String propertyName, PropertyChangeListener listener)
,
the propertyName
serves as a qualifier. In such cases, Mockobor utilizes a selector
.
It identifies 'selector' arguments in registration methods and allows the addition of selectors when sending notifications:
// In production code, the object under test registers its listener
public class ClassUnderTest {
...
void someInitMethod() {
...
observable.addMyListener( "q1", "q2", listner1 ); // ("q1", "q2") is the selector here
observable.addMyListener( "q3", listener2 ); // "q3" is the selector here
observable.addMyListener( listener3 ); // the selector is empty here
...
}
...
}
// In tests, send notifications to the listeners in the object under test.
class SomeTest {
...
@Test
void someTestMethod() {
...
// Send a notification to the listener registered with the selector ('q1', 'q2'):
notifier.notiferFor( listner1.class, selector( "q1", "q2" ) ).listener_method();
// Send a notification to the listener registered with the selector "q3":
notifier.notiferFor( "q3", listner2.class ).listener_method();
notifier.notifierFor( listener2.class, selector( "q3" ) ).listener_method(); // identical to the line above
// Send a notifications to the listener registered without a selector or with an empty selector:
notifier.notiferFor( listner3.class ).listener_method();
notifier.notifierFor( listener3.class, selector() ).listener_method(); // identical to the line above
// Send a notifications to the listeners registered with any of the specified selectors
// (here, all three: listener1, listener2 and listener3; see "someInitMethod" above)
notifier.notiferFor( listner1.class, selector( "q1", "q2" ), selector( "q3" ), selector() ).listener_method();
...
}
...
}
For more details see Examples / typical java style listeners
Typically, the listener notifier object is created prior to the tested object registering its listeners via the mocked observable. This method is reliable and compatible with all mocking tools:
class SomeTest {
private final SomeObservable mockedObservable = EasyMock.mock( SomeObservable.class );
private final ListenersNotifier notifier = Mockobor.createNotifierFor( mockedObservable ); // (1) the listener notifier created
private final TestedObserver testObject = new TestedObserver( mockedObservable ); // (2) the tested object registers its listeners
...
}
Alternatively, if you are using Mockito, you have the flexibility to create a listener notifier object whenever needed:
class SomeTest {
private final SomeObservable mockedObservable = Mockito.mock( SomeObservable.class );
private final TestedObserver testObject = new TestedObserver( mockedObservable );
@Test
void test_notifications() {
...
notifie = ockobor.createNotifierFor( mockedObservable );
...
notifier.notifierFor( XxxListener.class ).onChange(...);
...
}
}
It allows usage of Mockito annotations together with Mockobor. See Examples / use Mockito annotations and Mockobor together
Note: This is not compatible with EasyMock! See Restrictions / EasyMock restrictions
NotifierSettings
can be used to control the following aspects of the creation and behavior of a listener notifier:
- Strict or lenient checking to determine if the list of listeners selected for sending notifications contains any listeners:
- strict (default) - Throws a
ListenersNotFoundException
if no listener is selected to receive the notification. - lenient - Does nothing if no listener is selected.
- strict (default) - Throws a
- Interface implementation by a new listener notifier:
- true (default) - All new
ListenersNotifier
instances returned fromMockobor.createNotifierFor
implement all detected listener interfaces. This allows events to be fired using either of the following approaches:((MyListener) notifier).somethingChanged(...)
notifier.notifierFor(MyListener.class).somethingChanged(...)
- false - New
ListenersNotifier
instances do not implement listener interfaces. As a result, events can only be fired using the following approach:notifier.notifierFor(MyListener.class).somethingChanged(...)
- true (default) - All new
NotifierSettings
can be changed globally for all future ListenersNotifier
instances by modifying the settings stored statically in MockoborContext
:
MockoborContext.updateNotifierSettings().
ignoreListenerInterfaces().
lenientListenerListCheck();
or exclusively for one-time use during creation:
ListenersNotifier notifier = Mockobor.createNotifierFor(
mockedObservable,
Mockobor.notifierSettings().ignoreListenerInterfaces().lenientListenerListCheck();
For more details see UsageExample_NotifierSettings_Test.java
Out of the box Mockobor supports three kinds of listeners:
- Typical Java-style listeners, where listener class names follow the pattern
XxxListener
, and registration method names follow conventions likeaddXxxListener
andremoveXxxListener
(see Typical Java-style Listeners Example). java.beans.PropertyChangeListener
, considered a subclass of typical Java-style listeners ( see PropertyChangeListener Example).java.util.Observable
/java.util.Observer
(see Observable / Observer Example).
If you have a different type of listener, you can add support for it by following these steps:
- Create a custom implementation of
ListenerDefinitionDetector
. - Register it using the method
MockoborContext.registerListenerDefinitionDetector(yourListenerDetector)
.
To implement ListenerDefinitionDetector
, you typically need to extend AbstractDetector
and implement/override the following methods:
isListenerClass(Class, Method)
: Verifies if the specified parameter type is a listener type.isAddMethods(Method)
: Determines if the specified method is a listener registration method (for adding listeners).isRemoveMethods(Method)
: Determines if the specified method is a listener deregistration method (for removing listeners).getAdditionalInterfaces()
: (Optional) Use this if you want to provide special support for your listeners.getNotificationDelegates()
: (Optional) Use this if needed to:- Implement specific methods for your additional interfaces (it's often better to rely on default implementations in the interface itself).
- Override certain methods of
ListenersNotifier
(but consider if this is truly necessary).
For more details see JavaDoc (ListenerDefinitionDetector , AbstractDetector and MockoborContext.registerListenerDefinitionDetector)
See also Custom listener detector example, PropertyChangeDetector.java or TypicalJavaListenerDetector.java as implementation examples.
To redirect listener registration methods from a mocked observable object to the internal list of listeners, Mockobor requires the following:
- Understand which mocking tool was used to mock the specified observable object.
- Create a redirection mechanism using the detected mocking tool.
Mockobor provides out-of-the-box support for some mocking tools (see Dependencies).
If you are using a different mocking tool, you can add support for it by following these steps:
- Create a custom implementation of
ListenerRegistrationHandler
. - Register it using
MockoborContext.registerListenerRegistrationHandler
.
For more details see JavaDoc (ListenerRegistrationHandler and MockoborContext.registerListenerRegistrationHandler)
See also MockitoListenerRegistrationHandler.java and EasymockListenerRegistrationHandler.java as implementation examples.
-
Only interfaces are accepted as listeners:
Methods likeaddMyListener(MyListener)
will not be recognized as registration methods ifMyListener
is a class.
MyListener
must be an interface. This is standard practice in Java. -
Listener arrays and varargs are not supported:
Registration methods such asaddMyListener(MyListener[] listeners)
oraddMyListener(MyListener... listeners)
will also not be recognized as valid registration methods.
If you mock a collaborator object using EasyMock:
When mocking a collaborator object using EasyMock, keep the following restrictions in mind:
-
Notifier object creation:
A notifier object must be created (viaMockobor.createNotifierFor
) before the tested object registers its listener through the mocked collaborator.
This is because registration methods must be redirected to the notifier before being invoked by the tested object.
As a result, it is not possible to inject mocks into the observer test object using the@TestSubject
annotation. -
Issues with varargs in listener methods:
If listener registration methods of the mocked collaborator object utilize varargs (e.g.,addListener(MyListener l, Object... selector)
), issues may arise. During recording mode, Mockobor cannot predict how many arguments will be passed during the actual invocation of such a method. Consequently, recorded and real invocations may not match. For more details, refer to this issue.
To use Mockobor in your unit tests, add the following dependency to your project.
<dependency>
<groupId>io.github.mickle-ak.mockobor</groupId>
<artifactId>mockobor</artifactId>
<version>1.1.4</version>
<scope>test</scope>
</dependency>
testImplementation 'io.github.mickle-ak.mockobor:mockobor:1.1.4' // groovy
testImplementation("io.github.mickle-ak.mockobor:mockobor:1.1.4") // kotlin
For more details, refer to Maven Central.