package org.codefx.libfx.nesting;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import java.util.Optional;
import javafx.beans.Observable;
import javafx.beans.property.SimpleStringProperty;
import org.codefx.libfx.nesting.testhelper.NestingAccess;
import org.junit.Test;
/**
* Tests the class {@link NestingObserver}.
*/
public class NestingObserverTest {
// #begin INSTANCES USED FOR TESTING
/**
* The nesting's initial {@link Nesting#innerObservableProperty() innerObservable}.
*/
private Observable initialInnerObservable;
/**
* The observed {@link Nesting}.
*/
private NestingAccess.EditableNesting<Observable> nesting;
/**
* The {@link MethodCallVerifier} which called when the nesting changes.
*/
private MethodCallVerifier verifier;
//#end INSTANCES USED FOR TESTING
// #begin INITIALIZATION
/**
* Creates instances of {@link #initialInnerObservable}, {@link #nesting}, {@link #verifier} and a
* {@link NestingObserver} which uses the latter two.
*
* @param initiallyPresent
* indicates whether the created {@link #nesting} will have the {@link #initialInnerObservable} set or
* whether it will be missing
* @param resetMock
* indicates whether the mocked {@link #verifier} will be reset after the nesting observer is created
*/
public void setUpObservation(INNER_OBSERVABLE_INITIALLY_PRESENT initiallyPresent, RESET_MOCK resetMock) {
// create a nesting with the initial inner observable and mock a method call verifier
initialInnerObservable = new SimpleStringProperty();
if (initiallyPresent.toBoolean())
nesting = NestingAccess.EditableNesting.createWithInnerObservable(initialInnerObservable);
else
nesting = NestingAccess.EditableNesting.createWithInnerObservableNull();
verifier = mock(MethodCallVerifier.class);
// create a nesting observer for the nesting which calls the verifier's methods
NestingObserver
.forNesting(nesting)
.withOldInnerObservable(verifier::oldInnerObservableMethod)
.withNewInnerObservable(verifier::newInnerObservableMethod)
.whenInnerObservableChanges(verifier::innerObservableChangesMethod)
.observe();
// creating the observer already called some methods; reset the mock to not confuse the counts
if (resetMock.toBoolean())
reset(verifier);
}
//#end INITIALIZATION
// #begin TESTS
/**
* Tests whether the observer's construction leads to a correct initialization of the methods if the nesting's
* initial inner observable is present.
*/
@Test
public void testInitialCallsAfterConstructionWithPresentObservable() {
// create a nesting with the initial inner observable and mock a call verifier
setUpObservation(INNER_OBSERVABLE_INITIALLY_PRESENT.YES, RESET_MOCK.NO);
/*
* During its creation the observer must call some of the methods for a first time to initialize the state to
* match the nesting's state. There is never an old observable and in this case there is a new observable
* (namely 'initialInnerObservable').
*/
verify(verifier, times(1)).newInnerObservableMethod(initialInnerObservable);
verify(verifier, times(1)).innerObservableChangesMethod(false, true);
verifyNoMoreInteractions(verifier);
}
/**
* Tests whether the observer's construction leads to a correct initialization of the methods if the nesting's
* initial inner observable is missing.
*/
@Test
public void testInitialCallsAfterConstructionWithMissingObservable() {
// create a nesting with the no inner observable and mock a call verifier
setUpObservation(INNER_OBSERVABLE_INITIALLY_PRESENT.NO, RESET_MOCK.NO);
/*
* During its creation the observer must call some of the methods for a first time to initialize the state to
* match the nesting's state. There is never an old observable and in this case there also no new observable.
*/
verify(verifier, times(1)).innerObservableChangesMethod(false, false);
verifyNoMoreInteractions(verifier);
}
/**
* Tests whether replacing a missing inner observable with another missing one leads to correct method calls.
*/
@Test
public void testMissingPresentWithMissingInnerObservable() {
// create a nesting without an inner observable and mock a call verifier, which is then reset
setUpObservation(INNER_OBSERVABLE_INITIALLY_PRESENT.NO, RESET_MOCK.YES);
// set a new inner observable
nesting.setInnerObservable(Optional.empty());
// since both the old and the new observable are missing, the nesting did not really change,
// so no method should have been called
verifyZeroInteractions(verifier);
}
/**
* Tests whether replacing a missing inner observable with a present one leads to correct method calls.
*/
@Test
public void testReplacingMissingWithPresentInnerObservable() {
// create a nesting without an inner observable and mock a call verifier, which is then reset
setUpObservation(INNER_OBSERVABLE_INITIALLY_PRESENT.NO, RESET_MOCK.YES);
// set a new inner observable
Observable newInnerObservable = new SimpleStringProperty();
nesting.setInnerObservable(Optional.of(newInnerObservable));
// since only the new observable is present, the "changes"- and the "new"-method must be called:
verify(verifier, times(1)).newInnerObservableMethod(newInnerObservable);
verify(verifier, times(1)).innerObservableChangesMethod(false, true);
verifyNoMoreInteractions(verifier);
}
/**
* Tests whether replacing a present inner observable with a missing one leads to correct method calls.
*/
@Test
public void testReplacingPresentWithMissingInnerObservable() {
// create a nesting with the initial inner observable and mock a call verifier, which is then reset
setUpObservation(INNER_OBSERVABLE_INITIALLY_PRESENT.YES, RESET_MOCK.YES);
// set a new inner observable
nesting.setInnerObservable(Optional.empty());
// since only the old observable is present, the "changes"- and the "old"-method must be called:
verify(verifier, times(1)).oldInnerObservableMethod(initialInnerObservable);
verify(verifier, times(1)).innerObservableChangesMethod(true, false);
verifyNoMoreInteractions(verifier);
}
/**
* Tests whether replacing a present inner observable with another present one leads to correct method calls.
*/
@Test
public void testReplacingPresentWithPresentInnerObservable() {
// create a nesting with the initial inner observable and mock a call verifier, which is then reset
setUpObservation(INNER_OBSERVABLE_INITIALLY_PRESENT.YES, RESET_MOCK.YES);
// set a new inner observable
Observable newInnerObservable = new SimpleStringProperty();
nesting.setInnerObservable(Optional.of(newInnerObservable));
// since both the old and the new observable are present, all methods have to be called:
verify(verifier, times(1)).oldInnerObservableMethod(initialInnerObservable);
verify(verifier, times(1)).newInnerObservableMethod(newInnerObservable);
verify(verifier, times(1)).innerObservableChangesMethod(true, true);
verifyNoMoreInteractions(verifier);
}
//#end TESTS
// #begin INNER CLASSES
/**
* The {@link NestingObserver NestingObservers} created in this test call this class' methods when the nesting
* changes. The methods actually don't do anything; this class is only meant to be mocked so the correct interaction
* with the mock can be asserted with Mockito.
*/
@SuppressWarnings("unused")
private static class MethodCallVerifier {
/**
* Called when the {@link Nesting#innerObservableProperty() innerObservable} changes and the old inner
* observable was present.
*
* @param oldInnerObservable
* the old inner observable
*/
public void oldInnerObservableMethod(Object oldInnerObservable) {
// method is only used in mocking to count calls
}
/**
* Called when the {@link Nesting#innerObservableProperty() innerObservable} changes and the new inner
* observable is present.
*
* @param newInnerObservable
* the new inner observable
*/
public void newInnerObservableMethod(Object newInnerObservable) {
// method is only used in mocking to count calls
}
/**
* Called when the inner observable changes.
*
* @param oldInnerObservablePresent
* indicates whether the old inner observable was present
* @param newInnerObservablePresent
* indicates whether the new inner observable is present
*/
public void innerObservableChangesMethod(Boolean oldInnerObservablePresent, Boolean newInnerObservablePresent) {
// method is only used in mocking to count calls
}
}
/**
* Indicates whether {@link NestingObserverTest#setUpObservation(INNER_OBSERVABLE_INITIALLY_PRESENT, RESET_MOCK)
* setUpObservation} will create a nesting whose inner observable is initially present or not.
*/
private enum INNER_OBSERVABLE_INITIALLY_PRESENT {
/**
* The nesting's inner observable will initially be present.
*/
YES,
/**
* The nesting's inner observable will initially be missing.
*/
NO;
/**
* Returns a boolean value corresponding to this instances value.
*
* @return <code>true</code> if this is {@link #YES}; <code>false</code> if this is {@link #NO}
*/
public boolean toBoolean() {
switch (this) {
case YES:
return true;
case NO:
return false;
default:
throw new UnsupportedOperationException();
}
}
}
/**
* Indicates whether {@link NestingObserverTest#setUpObservation(INNER_OBSERVABLE_INITIALLY_PRESENT, RESET_MOCK)
* setUpObservation} will reset the mocked {@link NestingObserverTest#verifier verifier} after the observation was
* set up or not.
*/
private enum RESET_MOCK {
/**
* The verifier will be reset.
*/
YES,
/**
* The verifier will not be reset.
*/
NO;
/**
* Returns a boolean value corresponding to this instances value.
*
* @return <code>true</code> if this is {@link #YES}; <code>false</code> if this is {@link #NO}
*/
public boolean toBoolean() {
switch (this) {
case YES:
return true;
case NO:
return false;
default:
throw new UnsupportedOperationException();
}
}
}
//#end INNER CLASSES
}