Je suis très nouveau chez Mockito et jUnit et j'essaie d'apprendre la bonne façon de faire le TDD. J'ai besoin de couples d'exemple pour pouvoir écrire des tests unitaires avec mockito
Voici ma classe de contrôleurs qui télécharge un fichier et exécute une action sur les entrées de ce fichier.
@Controller
@RequestMapping("/registration")
public class RegistrationController {
@Autowired
private RegistrationService RegistrationService;
@Value("#{Properties['uploadfile.location']}")
private String uploadFileLocation;
public RegistrationController() {
}
@RequestMapping(method = RequestMethod.GET)
public String getUploadForm(Model model) {
model.addAttribute(new Registration());
return "is/Registration";
}
@RequestMapping(method = RequestMethod.POST)
public String create(Registration registration, BindingResult result,ModelMap model)
throws NumberFormatException, Exception {
File uploadedFile = uploadFile(registration);
List<Registration> userDetails = new ArrayList<Registration>();
processUploadedFile(uploadedFile,userDetails);
model.addAttribute("userDetails", userDetails);
return "registration";
}
private File uploadFile(Registration registration) {
Date dt = new Date();
SimpleDateFormat format = new SimpleDateFormat("MM_dd_yyyy_HH_mm_ss");
File uploadedFile = new File(uploadFileLocation
+ registration.getFileData().getOriginalFilename() + "."
+ format.format(dt));
registration.getFileData().transferTo(uploadedFile);
return uploadedFile;
}
private void processUploadedFile(File uploadedFile, List<Registration> userDetails)
throws NumberFormatException, Exception {
registrationService.processFile(uploadedFile, userDetails);
}
}
tout organisme peut-il suggérer un exemple, comment puis-je écrire un scénario de test pour cela avec mockito?
Éditer J'ai noté la classe de test suivante, mais comment continuer
@RunWith(MockitoJUnitRunner.class)
@ContextConfiguration(locations = { "/META-INF/spring/applicationContext.xml"})
public class BulkRegistrationControllerTest {
@InjectMocks
private RegistrationService registrationService= new RegistrationServiceImpl();
@Mock
private final ModelMap model=new ModelMap();
@InjectMocks
private ApplicationContext applicationContext;
private static MockHttpServletRequest request;
private static MockHttpServletResponse response;
private static RegistrationController registrationController;
@BeforeClass
public static void init() {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
registrationController = new RegistrationController();
}
public void testCreate()
{
final String target = "bulkRegistration";
BulkRegistration bulkRegistration=new BulkRegistration();
final BindingResult result=new BindingResult();
String nextPage=null;
nextPage = bulkRegistrationController.create(bulkRegistration, result, model);
assertEquals("Controller is not requesting the correct form",nextPage,
target);
}
}
Il semble que vous ayez oublié plusieurs choses dans votre test. Il existe des tests d'intégration et des tests unitaires. Les tests d'intégration testent tout (ou presque tout) du tout. Ainsi, vous utilisez des fichiers de configuration Spring très proches des fichiers réels et des exemples réels d'objets injectés dans votre classe sous test. C'est principalement ce que j'utilise @ContextConfiguration
, mais j'utilise cela conjointement avec @RunWith (SpringJUnit4ClassRunner.class)
Si vous utilisez Mockito (ou n’importe quel framework moqueur), c’est généralement parce que vous voulez isoler la classe que vous testez des implémentations réelles d’autres classes. Ainsi, au lieu de devoir, par exemple, trouver un moyen de faire en sorte que RegistrationService lève une exception NumberFormatException pour tester ce chemin de code, vous indiquez simplement à RegistrationService de le faire. Il existe de nombreux autres exemples où il est plus pratique d'utiliser des simulacres que d'utiliser des instances de classe réelles.
Donc, cette mini-leçon est terminée. Voici comment je voudrais réécrire votre classe de test (avec un exemple supplémentaire et commenté en cours de route).
@RunWith(MockitoJUnitRunner.class)
public class RegistrationControllerTest {
// Create an instance of what you are going to test.
// When using the @InjectMocks annotation, you must create the instance in
// the constructor or in the field declaration.
@InjectMocks
private RegistrationController controllerUT = new RegistrationController();
// The @Mock annotation creates the mock instance of the class and
// automatically injects into the object annotated with @InjectMocks (if
// possible).
@Mock
private RegistrationService registrationService;
// This @Mock annotation simply creates a mock instance. There is nowhere to
// inject it. Depending on the particular circumstance, it may be better or
// clearer to instantiate the mock explicitly in the test itself, but we're
// doing it here for illustration. Also, I don't know what your real class
// is like, but it may be more appropriate to just instantiate a real one
// than a mock one.
@Mock
private ModelMap model;
// Same as above
@Mock
private BulkRegistration bulkRegistration;
// Same as above
@Mock
private FileData fileData;
@Before
public void setUp() {
// We want to make sure that when we call getFileData(), it returns
// something non-null, so we return the mock of fileData.
when(bulkRegistration.getFileData()).thenReturn(fileData);
}
/**
* This test very narrowly tests the correct next page. That is why there is
* so little expectation setting on the mocks. If you want to test other
* things, such as behavior when you get an exception or having the expected
* filename, you would write other tests.
*/
@Test
public void testCreate() throws Exception {
final String target = "bulkRegistration";
// Here we create a default instance of BindingResult. You don't need to
// mock everything.
BindingResult result = new BindingResult();
String nextPage = null;
// Perform the action
nextPage = controllerUT.create(bulkRegistration, result, model);
// Assert the result. This test fails, but it's for the right reason -
// you expect "bulkRegistration", but you get "registration".
assertEquals("Controller is not requesting the correct form", nextPage,
target);
}
/**
* Here is a simple example to simulate an exception being thrown by one of
* the collaborators.
*
* @throws Exception
*/
@Test(expected = NumberFormatException.class)
public void testCreateWithNumberFormatException() throws Exception {
doThrow(new NumberFormatException()).when(registrationService)
.processFile(any(File.class), anyList());
BindingResult result = new BindingResult();
// Perform the action
controllerUT.create(bulkRegistration, result, model);
}
}
Il est certainement possible d'écrire des tests unitaires purs pour les contrôleurs Spring MVC en se moquant de leurs dépendances avec Mockito (ou JMock), comme le montre jherricks ci-dessus. Le problème qui reste est que, avec les contrôleurs POJO annotés, il en reste beaucoup qui n’ont pas été testés - essentiellement tout ce qui est exprimé en annotations et effectué par le cadre lorsque le contrôleur est appelé.
La prise en charge des tests des contrôleurs Spring MVC est en cours (voir le projet spring-test-mvc ). Bien que le projet subisse encore des modifications, il est utilisable dans sa forme actuelle. Si vous êtes sensible au changement, vous ne devriez pas en dépendre. Quoi qu’il en soit, j’ai pensé que cela valait la peine d’être souligné si vous voulez le suivre ou participer à son développement. Il y a un instantané nocturne et il y aura une publication de jalon ce mois-ci si vous souhaitez verrouiller une version spécifique.
Mockito est un framework moqueur utilisé pour se moquer d'objets. Cela est généralement faisable lorsque vous testez une méthode qui dépend du résultat de la méthode d'un autre objet. Par exemple, lors du test de votre méthode de création, vous voudrez simuler la variable uploadedFile
, car ici vous n'êtes pas intéressé à tester si la fonction uploadFile(Registration registration)
fonctionne correctement (vous la testez dans un autre test), mais vous êtes intéressé à si la méthode traite le fichier téléchargé et si elle ajoute la variable details
au modèle. Pour simuler le fichier téléchargé, vous pouvez utiliser: when(RegistrationController.uploadFile(anyObject()).thenReturn(new File());
Mais ensuite, vous voyez que cela montre un problème de conception. Votre méthode uploadFile()
ne doit pas résider dans le contrôleur, mais dans une autre classe d’utilitaires. Et puis vous pourriez @Mock cette classe d'utilitaire au lieu du contrôleur.
N'oubliez pas que si votre code est difficile à tester, cela signifie que vous n'avez pas fait de votre mieux pour rester simple.
Je ne connais pas bien Mockito (parce que j’utilise JMock ), mais l’approche générale d’écriture de tests avec mock est la même.
Tout d'abord, vous avez besoin d'une instance de la classe à tester (CUT) (RegistrationController
name__). Cela ne doit PAS être une simulation - parce que vous voulez le tester.
Pour tester getUploadForm
name__, l'instance CUT n'a besoin d'aucune dépendance, vous pouvez donc la créer via new RegistrationController
.
Ensuite, vous devriez avoir un chapeau de test qui ressemble un peu à ceci
RegistrationController controller = new RegistrationController();
Model model = new Model();
String result = controller(model);
assertEquals("is/Registration", result);
assertSomeContstrainsFormodel
C'était facile.
La méthode suivante que vous souhaitez tester est create
name__, Method. C'est beaucoup plus difficile.
BindingResult
name__) peut-être un peu plus compliquéregistrationService
et uploadFileLocation
- c’est la partie intéressante.uploadFileLocation
est simplement un champ qui doit être défini dans le test. Le moyen le plus simple serait d’ajouter un setter (getter et) pour définir le classement dans le test. Vous pouvez également utiliser le org.springframework.test.util.ReflectionTestUtils
pour définir ce champ. - Les deux manières ont des avantages et des inconvénients.
Plus intéressant est registrationService
name__. Cela devrait être une maquette! Vous devez créer une maquette pour cette classe, puis "injecter" cette maquette dans l'instance CUT. Comme pour le uploadFileLocation
name__, vous avez au moins les deux mêmes choix.
Ensuite, vous devez définir les exceptions que vous avez pour le modèle: que registrationService.processFile(uploadedFile, userDetails)
est appelé avec les détails de fichier et d’utilisateur corrects. (La précision de cette exception fait partie de Mockito - et je n'ai pas assez de connaissances).
Ensuite, vous devez invoquer la méthode que vous souhaitez tester sur CUT.
BTW: Si vous devez "injecter" des simulacres sur des haricots de printemps très souvent, vous pouvez créer votre propre util. Pour obtenir une instance d'objet, recherchez des champs avec des annotations @Inject
dans cet objet, créez des simulations pour cela et "injectez" pour simuler. (Ensuite, vous n'avez besoin que de getter pour accéder aux simulacres et définir leurs expulsions.) - J'ai construit un tel outil pour JMock, qui m'a beaucoup aidé.
En examinant votre exemple de code ci-dessus, je vois quelques problèmes:
Le but de Mockito est de se moquer des dépendances de votre classe. Cela vous permettra d'utiliser un scénario de test JUnit simple. Par conséquent, il n'est pas nécessaire d'utiliser @ContextConfiguration. Vous devriez pouvoir instancier la classe en cours de test à l'aide du nouvel opérateur, puis fournir les dépendances requises.
Vous utilisez Autowiring pour fournir votre service d’enregistrement. Pour injecter une instance fictive de ce service, vous devez utiliser les utilitaires d’accès au champ privé de test Spring.
Je ne vois pas dans votre code si RegistrationService est une interface. Si ce n'est pas le cas, vous aurez du mal à vous en moquer.
La vraie question est: comment configurer un environnement de test de votre application qui utilise Spring? La réponse à cette question n’est pas simple, cela dépend vraiment du fonctionnement de votre application Web.
Vous devez d’abord vous concentrer sur la manière de JUniter une application Web Java, puis sur l’utilisation de Mockito.
Suggestion alternative: n'utilisez pas Mockito. Spring propose ses propres classes de test que vous pouvez utiliser pour vous moquer et vous pouvez utiliser le SpringJUnit4ClassRunner
. L'utilisation du programme d'exécution de test Spring JUnit vous permet de charger une configuration Spring complète (via @ContextConfiguration
) ainsi que de simuler des objets. Dans votre cas, une grande partie de votre code d'instanciation disparaît, car vous exécuterez Spring sans imiter sa DI.
Essaye ça.
@ RunWith (SpringJUnit4ClassRunner.class) @ ContextConfiguration (locations = {"/META-INF/spring/applicationContext.xml"Name)&. ] @Mock Private RegistrationService registrationService; // Contrôleur en cours de test. @Autowired @InjectMocks private RegistrationController registrationController; @Avant public void setUp () { MockitoAnnotations.initMocks (this); .. . } ...