I have been thinking quite some time and asked an StackOverflow question about extending abstract test classes, but I haven't been able to do it until now.
I'll list my approach and then discuss some pros and cons and am asking you for a review of the code.
Consider the PlayerAction
interface:
public interface PlayerAction {
boolean isActionAllowed(final Player player);
void performAction(final Player player) throws PlayerActionNotAllowedException;
}
In the invisible documentation I have written that in both methods that player != null
must hold.
The abstract test class that should be extended, is the following:
@Ignore
public abstract class PlayerActionAbstractTest {
static {
assertTrue(true);
}
private final Supplier<PlayerAction> playerActionSupplier;
private PlayerAction playerAction;
public PlayerActionAbstractTest() {
this.playerActionSupplier = null;
}
protected PlayerActionAbstractTest(final Supplier<PlayerAction> playerActionSupplier) {
this.playerActionSupplier = Objects.requireNonNull(playerActionSupplier, "playerActionSupplier");
}
@Before
final public void beforePlayerActionAbstractTest() {
playerAction = playerActionSupplier.get();
}
@Test(expected = NullPointerException.class)
public void testIsActionAllowedNullPlayer() {
playerAction.isActionAllowed(null);
}
@Test(expected = NullPointerException.class)
public void testPerformActionNullPlayer() {
playerAction.performAction(null);
}
}
An concrete implementation, I'll spare you all most irrelevant code:
public class AttackMonsterActionTest extends PlayerActionAbstractTest {
private Player self;
private Player opponent;
private final static PlayerConfiguration PLAYER_CONFIGURATION = new PlayerConfigurationBuilder()
.hitpoints(100)
.turnAction(p -> { })
.handCapacity(5)
.fieldMonsterCapacity(5)
.deckCards(Arrays.asList(new MonsterCard("Random", 5, 5, MonsterModus.HEALING)))
.build();
public AttackMonsterActionTest() {
super(() -> new AttackMonsterAction(0, 0, Player.createFromConfiguration(PLAYER_CONFIGURATION, "Opponent")));
}
@Before
public void before() {
self = Player.createFromConfiguration(PLAYER_CONFIGURATION, "Self");
opponent = Player.createFromConfiguration(PLAYER_CONFIGURATION, "Opponent");
}
@Test
public void testConstructor() {
new AttackMonsterAction(0, 0, opponent);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorNegativeMonsterIndex() {
new AttackMonsterAction(-1, 0, opponent);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorNegativeTargetMonsterIndex() {
new AttackMonsterAction(0, -1, opponent);
}
@Test(expected = IllegalArgumentException.class)
public void testConstructorTooHighTargetMonsterIndex() {
new AttackMonsterAction(0, 5, opponent);
}
@Test(expected = NullPointerException.class)
public void testConstructorNullPlayer() {
new AttackMonsterAction(0, 0, null);
}
//more custom tests
}
And it now executes the tests from PlayerActionAbstractTest
on my AttackMonsterActionTest
class!
The pros are that the instance only needs to be passed in once and most importantly that you must pass it in or you will get a compiler error. Another pro is that the list of classes to be tested is not defined in the abstract testclass itself.
The cons possibly are that issues can arise with dependencies and mock objects, but I'm not too sure about that and would love to have that included in a review.
Does this seem to be a reasonable way to do it?
Small update, changed signature in PlayerActionAbstractTest
from public void superBefore()
to final public void superBefore()
to ensure that a compiler errors, rather than warning, will be thrown on an accidental override.
Second small update, changed signature in PlayerActionAbstractTest
from final public void superBefore()
to private void before()
, now it cannot be overridden anymore.
Last update, appereantly the latest change was showing weird behaviours when executing unit tests on my whole project, rather than a single file. I have changed the following:
- Changed signature in
PlayerActionAbstractTest
fromprivate void before()
tofinal public void beforePlayerActionAbstractTest()
, as the@Before
method needs to bepublic
and in case inheriting multiple times is possible,beforeSuper()
would clash. - Changed the
public PlayerActionAbstractTest(final Supplier<PlayerAction> playerActionSupplier)
constructor toprotected PlayerActionAbstractTest(final Supplier<PlayerAction> playerActionSupplier)
, to fix that a test class must only have one public zero-args constructor. - Added
public PlayerActionAbstractTest()
to make it have one public zero-args constructor. - Added
@Ignore
to ensure that this abstract class itself isn't being tested.