问题
I have 2 different objects: C c and B b. B and C implement interface A so that they can use method called color() that is present in interface A. I already made unit test for class B that test the color() method that B has implemented. So what I want to do now is test the color() method in class C with the same unit test of class B. Thus I want to test both of them in the same test class that I have made for class B.
To achieve this, one of my friends said that I would have to make use of the parallel class hierarchy of those classes. But I don't really know how I should implement that in my test.
This is what I have in terms of code:
private static Sprites sprites;
private static B b;
@BeforeAll
static void setUp() {
sprites = mock(Sprites.class);
b = new B(sprites);
}
@Test
void testing_color_method() {
M m = mock(M.class);
b.color(m);
verify(sprites).getSpritesOf(m);
}
//... some more tests
I am using JUnit 5 and I know I could use @ParameterizedTest to inject objects B and C in the test to let them use the same unit tests, I have also done some google search about this but none of the search results had it about this kinds of cases where 2 objects need to be injected in the 1 test. So how should I refactor the code so that I can inject class B and C to let them use the same unit tests that I already crated for B?
回答1:
Using the same tests for B and C means, you are in fact testing via the interface A. These would be then be primarily black-box tests, because your tests can only depend on elements of the interface, but not elements of the specific implementations in B and C. More about this below.
First, however, some example code how you could achieve your goal: The approach uses the Junit 5 parameterized tests using @MethodSource. In the example, the @MethodSource provides as arguments a) a description of the test (which is not used in the test method logic and thus called dummy there), b) an instance of one of the classes which implement interface A, and, for the sake of a working example you can experiment with, c) the object's expected class name.
Code for interface A:
package so56386880;
public interface A {
public void color();
}
Code for classes B and C (I used the same code, showing B here):
package so56386880;
public class B implements A {
public void color() { }
}
Test Code
package so56386880;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
class A_Test {
@DisplayName("Test one common property of color method")
@ParameterizedTest(name = "{index}: {0}")
@MethodSource("provideImplementors")
void color_whenCalled_shallXxx(String dummy, A someA, String expectedClassName) {
// you would do some test for someA.color(), but just for example:
assertEquals(expectedClassName, someA.getClass().getName());
}
static Stream<Arguments> provideImplementors() {
return Stream.of(
Arguments.of("expecting class B's name", new B(), "so56386880.B"),
Arguments.of("expecting class C's name", new C(), "so56386880.C"));
}
}
As said above, this can only perform tests that are restricted to using the interface A. Since B and C will have different implementations, it is likely that you also need some specific tests for B and C. Keep in mind that the goal of testing is to find bugs - and different implementations typically have different potential bugs (different control flows, different possibilities for overflows, etc.).
Update: The solution above answers how to do it with parameterized tests that provide the class under test as an argument. An alternative approach could be to implement a test class for the interface as below:
package so56386880;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
abstract class A_Test2 {
public abstract A getClassUnderTest();
@Test
void color_whenCalled_shallXxx() {
A classUnderTest = this.getClassUnderTest();
classUnderTest.color(); // exercise
assertEquals(expected..., actual...);
}
}
And then derive from it, like shown below:
package so56386880;
class B_Test extends A_Test2 {
public A getClassUnderTest() { return new B(); }
}
When running B_Test under Junit 5, all test methods in the base class get executed (plus additional ones if defined within B_Test). The advantage is, that you don't have to modify A_Test2 whenever a new derived class is created. If you have, however, expected values that differ for each derived class (as was the case with the class name in the first solution), this requires some adaption, for example more callback methods similar to getClassUnderTest.
来源:https://stackoverflow.com/questions/56386880/how-to-test-multiple-objects-with-same-method-in-1-test-class