I learning Spring Boot web application with .jsp, and I'm struggling a lot with the testing concepts. From the SO and YT guides I implemented the Mockito thing, but honestly I do not clearly undesrtand how does it work.
I have a Registration form with 4 fields for name, lastname, email and password. This POST request is handled by the registerAction method in RegisterController. In this method I have two self-written validators for email and password. The tests should handle the cases when User data are given properly and if the errors are sent when inputs are not correct.
I tried to write tests for the controller but I'm constantly getting an exception NullPointerExpection. Looking into the debugger, the User object sent from the testing class has null attributes, which probably is the reason the exceptions.
Testing class:
#SpringBootTest
#AutoConfigureMockMvc
class RegisterControllerTest {
#Autowired
private WebApplicationContext wac;
#MockBean
private UserService userService;
#Autowired
private Gson gson;
#Autowired
private MockMvc mockMvc;
#BeforeEach
void setUp() {
initMocks(this);
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.apply(springSecurity()).build();
}
#Test
void show_register_action() throws Exception {
User user = prepareUserEmpty();
this.mockMvc.perform(post("/adduser")
.contentType(MediaType.APPLICATION_JSON)
.content(gson.toJson(user)))
.andDo(print())
.andExpect(status().isOk());
}
private User prepareUserEmpty(){
User user = new User();
user.setEmail("");
user.setPassword("");
user.setName("");
user.setLastName("");
return user;
}
}
RegisterController:
#Controller public class RegisterController {
#Autowired
private UserService userService;
#Autowired
private EmailSender emailSender;
#Autowired
MessageSource messageSource; // Allows to obtain messages from message.properties to Java code
#POST
#RequestMapping(value = "/adduser")
public String registerAction(User user, BindingResult result, Model model, Locale locale){ // BindingResult for validation, Locale for messageSource
String returnPage = "register";
User userExist = userService.findUserByEmail(user.getEmail());
new UserRegisterValidator().validate(user, result);
new UserRegisterValidator().validateEmailExist(userExist, result);
if (!(result.hasErrors())){
userService.saveUser(user);
model.addAttribute("message", messageSource.getMessage("user.register.success.email", null, locale));
returnPage = "index";
}
return returnPage;
} }
Validators:
public class UserRegisterValidator implements Validator {
#Override
public boolean supports(Class <?> cls){
return User.class.equals(cls);
}
#Override
public void validate(Object obj, Errors errors){
User u = (User) obj;
ValidationUtils.rejectIfEmpty(errors, "name", "error.userName.empty");
ValidationUtils.rejectIfEmpty(errors, "lastName", "error.userLastName.empty");
ValidationUtils.rejectIfEmpty(errors, "email", "error.userEmail.empty");
ValidationUtils.rejectIfEmpty(errors, "password", "error.userPassword.empty");
if (!u.getEmail().equals(null)){
boolean isMatch = AppdemoUtils.checkEmailOrPassword(AppdemoConstants.EMAIL_PATTERN, u.getEmail());
if (!isMatch)
errors.rejectValue("email", "error.userEmailIsNotMatch");
}
if (!u.getPassword().equals(null)){
boolean isMatch = AppdemoUtils.checkEmailOrPassword(AppdemoConstants.PASSWORD_PATTERN, u.getPassword());
if (!isMatch)
errors.rejectValue("password", "error.userPasswordIsNotMatch");
}
}
public void validateEmailExist(User user, Errors errors){
if (user != null)
errors.rejectValue("email", "error.userEmailExist");
}
}
Related
I have the following problem:
I want to test if a user registers successfully. I'm not doing this over a UI but via Postman. The problem is that I get an exception when I use the POST Command. I have already checked all annotations.
Postman Screenshot
Exception:
java.lang.IllegalArgumentException: rawPassword cannot be null
at org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.encode(BCryptPasswordEncoder.java:103) ~[spring-security-core-5.3.3.RELEASE.jar:5.3.3.RELEASE]
at com.example.application.backend.data.service.UserService.singUpUser(UserService.java:41) ~[classes/:na]
at com.example.application.backend.data.registration.RegistrationService.register(RegistrationService.java:22) ~[classes/:na]
at com.example.application.backend.data.registration.RegistrationController.register(RegistrationController.java:18)
This is my UserService Class:
private final static String USER_NOT_FOUND_MSG = "user with email %s not found";
#Autowired
private final UserRepository userRepository;
#Autowired
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public UserService(
UserRepository userRepository,
BCryptPasswordEncoder bCryptPasswordEncoder) {
this.userRepository = userRepository;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
}
#Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.orElseThrow(() ->
new UsernameNotFoundException(String.format(USER_NOT_FOUND_MSG, email)));
}
public String singUpUser(User user) {
boolean userExists = userRepository.findByEmail(user.getEmail()).isPresent();
if(userExists){
throw new IllegalStateException("email already taken");
}
String encodedPassword = bCryptPasswordEncoder.encode(user.getPassword());
user.setPassword(encodedPassword);
userRepository.save(user);
return "it works";
}
Registration Service Class:
#Autowired
private final UserService userService;
private final EmailValidator emailValidator;
public String register(RegistrationRequest request) {
boolean isValidEmail = emailValidator.test(request.getEmail());
if (!isValidEmail) {
throw new IllegalStateException("email not valid");
}
return userService.singUpUser(
new User(
request.getFirstName(),
request.getLastName(),
request.getEmail(),
request.getPassword(),
UserRole.USER
)
);
}
public RegistrationService(UserService userService, EmailValidator emailValidator) {
this.userService = userService;
this.emailValidator = emailValidator;
}
Registration Controller Class:
#Autowired
private RegistrationService registrationService;
#PostMapping
public String register(#RequestBody RegistrationRequest request) {
return registrationService.register(request);
}
This error occurs when the password string that is fed to the bCryptEncoder is empty or null.
In your UserService.java class,
String encodedPassword = bCryptPasswordEncoder.encode(user.getPassword());
user.getPassword() is null, which means in RegistrationService.java class,
request.getPassword()
is fetching a null value.
Please check if you are getting the correct parameter name for password from the request object. If possible, add some logger statements in your code and debug.
GitHub PR
It is working. The problem was that the #Column annotations were set to nullable false.
Expanding #Sidharth's answer.
For anyone experiencing this problem check your password getter at AppUser.java class. It is probably returning null.
Change
public String getPassword(){return null;}
To
public String getPassword(){return password;}
please pay attention when we import the module request body
make sure the import is org.springframework.web.bind.annotation.RequestBody;
So here is what the current description of the error gives me.
Description:
Field profileDoa in com.N2O2.Nitrouz_Studioz.controller.MainController required a bean of type 'com.N2O2.Nitrouz_Studioz.model.profile.ProfileDoa' that could not be found. The injection point has the following annotations:
- #org.springframework.beans.factory.annotation.Autowired(required=true)
Action:
Consider defining a bean of type 'com.N2O2.Nitrouz_Studioz.model.profile.ProfileDoa' in your configuration.
I'm sort of lost to why this error is happening as I've only been working with Java Spring Boot recently and still getting used to working with Beans. I've Autowired the Bean in the Test class but it's still throwing the same error.
Here's what I have in my test class and the Controller and ProfileDoa class.
#WebMvcTest(MainController.class)
#ContextConfiguration(classes = NitrouzStudiozApplication.class)
public class MainControllerTest {
#Autowired
private MockMvc mockMvc;
ProfileEntity profileEntity;
#Autowired
private ProfileDoa profileDoa;
private MainController mainController;
#Mock
private Model model;
private boolean loggedOut = true;
private boolean loggedIn = false;
#BeforeEach
public void intializeController(){
mainController = new MainController();
}
#Test
#DisplayName("Navigating to Website Correctly Displays Index page")
public void loadsIndexPage() throws Exception {
RequestBuilder request = MockMvcRequestBuilders.get("/");
MvcResult result = mockMvc.perform(request)
.andExpect(model().attribute("loggedOut", loggedOut))
.andExpect(model().attribute("loggedIn", loggedIn))
.andExpect(model().attribute("profileEntity", "Not logged In"))
.andReturn();
Assertions.assertEquals("index", result);
}
}
#Controller
public class MainController {
private boolean loggedOut = true;
private boolean loggedIn = false;
private ProfileEntity profileEntity;
#Autowired
private ProfileDoa profileDoa;
#RequestMapping("/")
public String home_page(Model model) {
model.addAttribute("loggedOut", loggedOut);
model.addAttribute("loggedIn", loggedIn);
model.addAttribute("profileEntity", "Not logged In");
return "index";
}
#RequestMapping("/about")
public String about_page(Model model){
model.addAttribute("loggedOut", loggedOut);
model.addAttribute("loggedIn", loggedIn);
model.addAttribute("profileEntity", "Not logged In");
return "about";
}
#RequestMapping("/signup")
public String sign_up(){
return "signup";
}
#GetMapping("/signUpForm")
public String signUpForm(Model model, ProfileEntity profileEntity){
boolean checked = false;
model.addAttribute("profileEntity", profileEntity);
model.addAttribute("join", checked);
return "signUpForm";
}
#RequestMapping("/signUpFormError")
public String signUpFormError(Model model,
#ModelAttribute("error") boolean error,
#ModelAttribute("message") String message,
ProfileEntity profileEntity){
boolean checked = false;
model.addAttribute("join", checked);
model.addAttribute("error", error);
model.addAttribute("message", message);
model.addAttribute("profileEntity", profileEntity);
return "signUpForm";
}
#RequestMapping("/ForgotPasswordPage")
public String forgotPasswordPage(){
return "forgotPassword";
}
#GetMapping("/Forgot_Password")
public String ForgotPasswordResponse(){
return "forgotPassword";
}
}
#Transactional
public interface ProfileDoa extends JpaRepository<ProfileEntity, Long> {
public ProfileEntity findByEmail(String email);
}
Any help on this would be helpful. Thanks.
With #WebMvcTest you can write tests for your web layer. Spring Test will create a context for you with all beans that are required to test this slice of your application: e.g. classes annotated with #Controller, #RestController, #ControllerAdvice, filter, etc.
All other beans are not created for you as they are not in the scope of the web layer. In your case, that's any other bean your MainController injects.
You have basically now two options:
Mock the ProfileDoa
Use a real bean of ProfileDoa. This would require a database and more setup on your side.
For option one you can adjust your test like the following:
#WebMvcTest(MainController.class)
public class MainControllerTest {
#Autowired
private MockMvc mockMvc;
#MockBean
private ProfileDoa profileDoa;
private boolean loggedOut = true;
private boolean loggedIn = false;
#Test
#DisplayName("Navigating to Website Correctly Displays Index page")
public void loadsIndexPage() throws Exception {
RequestBuilder request = MockMvcRequestBuilders.get("/");
MvcResult result = mockMvc.perform(request)
.andExpect(model().attribute("loggedOut", loggedOut))
.andExpect(model().attribute("loggedIn", loggedIn))
.andExpect(model().attribute("profileEntity", "Not logged In"))
.andReturn();
Assertions.assertEquals("index", result);
}
}
As I don't see any interaction with profileDoa in your MainController there is also no need to prepare any mocked method response. If you do however call e.g. profileDao.findByEmail("mail#duke.io") somewhere, you can use Mockito to prepare the result:
ProfileEntity databaseResult = new ProfileEntitiy();
when(profileDao.findByEmail("THIS_HAS_TO_MATCH_YOUR_MAIL")).thenReturn(databaseResult);
For option two, you can use a combination of #SpringBootTest and #AutoconfigureMockMvc to load the whole Spring context (all beans) and make use of MockMvc:
#SpringBootTest
#AutoconfigureMockMvc
public class MainControllerTest {
// no mocking required as _real_ beans are used
}
Here you might want to use e.g. Testcontainers to start a database for your test.
I want to use user-information after login succeed. I thought about storing it into session attribute. or using #scope('session) annotation. but I haven't found the best way of doing it. so I just stored it into model-attribute.
#Controller
#RequestMapping(value = "/user")
public class UserController {
#Autowired
private UserService userService;
#Autowired
private UserProfileService userProfileService;
#ModelAttribute("user")
public User getUserModel () {
return userService.findByEmail(SecurityContextHolder.getContext().getAuthentication().getName());
}
#ModelAttribute("userProfile")
public UserProfile getUserProfile() {
return userProfileService.findByUser(userService.findByEmail(SecurityContextHolder.getContext().getAuthentication().getName()));
}
#GetMapping("/")
public String userIndex() {
logger.info("UserIndex");
return "userPage";
}
As you can see, SecurityContextHolder.getContext().getAuthentication().getName() --> this method repeated. every time user make HTTP request, is this good practice? or any better way of store user-infomation in application?
I would go with this.
#RequestMapping(value = "/username", method = RequestMethod.GET)
#ResponseBody
public String currentUserName(Authentication authentication) {
return authentication.getName();
}
I have a test class that looks like this
I found out that the test when i use #WebMvcTest Does not recognize the url for some reason.Any help would suffice please .
After debugging i get that DefaultRequestBuilder =Null, DefaultRequestMatcher size=0
#AutoConfigureMockMvc()
#RunWith(SpringJUnit4ClassRunner.class)
#WebMvcTest({ShoppingCartController.class, HomeController.class})
#ContextConfiguration(classes = {SecurityConfig.class})
public class ShoppingCartControllerTest {
#Autowired
WebApplicationContext context;
#Autowired
private MockMvc mockMvc;
#MockBean
private BookService bookService;
#MockBean
private UserService userService;
#MockBean
private CartItemService cartItemService;
#Before
public void setUp() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
#Test
public void showLoginPage() throws Exception {
mockMvc.perform(get("/login")
.accept(MediaType.TEXT_HTML)
.contentType(MediaType.TEXT_HTML)
)
.andExpect(model().attributeExists("classActiveLogin"))
.andReturn();
}
#Test
#WithMockUser(username = "V", authorities = {"USER"})
public void addItemToShoppingCart() throws Exception {
CartItem cartItem = new CartItem();
String qty = "2";
Book book = new Book();
User user = new User();
book.setId(1L);
book.getId();
cartItem.setBook(book);
when(userService.findByUsername(anyString())).thenReturn(user);
when(bookService.findOne(anyLong())).thenReturn(book);
when(cartItemService.addBookToCartItem(book, user, Integer.parseInt(qty))).thenReturn(cartItem);
ObjectMapper mapper = new ObjectMapper();
String bookAsString = mapper.writeValueAsString(book);
mockMvc
.perform(get("/shoppingCart/addItem")
.accept(MediaType.TEXT_HTML)
.contentType(MediaType.TEXT_HTML)
.param("book", bookAsString)
.param("qty", qty))
.andReturn();
}
#Configuration
#Import({PropertyTestConfiguration.class, SecurityUtility.class})
static class ContextConfiguration {
}
}
Only the test addItemToShoppingcCart passes , the other three have the same error as shown below , It is definitely not importing everything I need but I cant figure out what exactly it is.
java.lang.AssertionError: No ModelAndView found
at org.springframework.test.util.AssertionErrors.fail(AssertionErrors.java:35)
at org.springframework.test.util.AssertionErrors.assertTrue(AssertionErrors.java:65)
I don't understand why I t cant find models and views but when I use #
SpringBootTest and EasyMock everything passes ? what can I do ?
These are my controllers
#RequestMapping("/login")
public String login(Model model) {
model.addAttribute("classActiveLogin", true);
return "Myaccount";
}
#PreAuthorize("hasAuthority('USER')")
#RequestMapping("/addItem")
public String addItem(
#ModelAttribute("book") Book book,
#ModelAttribute("qty") String qty,
Model model, Principal principal
) {
User user = userService.findByUsername(principal.getName());
try {
book = bookService.findOne(book.getId());
if (Integer.parseInt(qty) > book.getInStockNumber()) {
model.addAttribute("notEnoughStock", true);
return "forward:/bookDetail?id=" + book.getId();
}
CartItem cartItem = cartItemService.addBookToCartItem(book, user, Integer.parseInt(qty));
model.addAttribute("addBookSuccess", true);
} catch (NullPointerException e) {
}
return "forward:/bookDetail?id=" + book.getId();
}
I have two controllers i am testing
#Controller
public class HomeController {
#Autowired
private JavaMailSender mailSender;
#Autowired
private MailConstructor mailConstructor;
#Autowired
private UserService userService;
#RequestMapping("/login")
public String login(Model model) {
model.addAttribute("classActiveLogin", true);
return "Myaccount";
}
And Second one which has Class level Mapping
#Controller
#RequestMapping("/shoppingCart")
public class ShoppingCartController {
#Autowired
private UserService userService;
// autowired services here
#PreAuthorize("hasAuthority('USER')")
#RequestMapping("/addItem")
public String addItem(
#ModelAttribute("book") Book book,
#ModelAttribute("qty") String qty,
Model model, Principal principal
) {
User user = userService.findByUsername(principal.getName());
try {
book = bookService.findOne(book.getId());
if (Integer.parseInt(qty) > book.getInStockNumber()) {
model.addAttribute("notEnoughStock", true);
return "forward:/bookDetail?id=" + book.getId();
}
CartItem cartItem = cartItemService.addBookToCartItem(book, user, Integer.parseInt(qty));
model.addAttribute("addBookSuccess", true);
} catch (NullPointerException e) {
}
return "forward:/bookDetail?id=" + book.getId();
}
I am trying to write the test case for test controller, here is the code for the controller
#Controller
#RequestMapping("/")
#SessionAttributes({"roles", "departments"})
public class AppController {
#Autowired
UserService userService;
#Autowired
RoleService roleService;
#Autowired
DepartmentService departmentService;
#Autowired
MessageSource messageSource;
#Autowired
PersistentTokenBasedRememberMeServices persistentTokenBasedRememberMeServices;
#Autowired
AuthenticationTrustResolver authenticationTrustResolver;
static final Logger logger = LoggerFactory.getLogger(AppController.class);
/**
* This method will list all existing users.
*/
#RequestMapping(value = { "/", "/list" }, method = RequestMethod.GET)
public String listUsers(ModelMap model) {
List<User> users = userService.findAllUsers();
model.addAttribute("users", users);
model.addAttribute("loggedinuser", getPrincipal());
return "userslist";
}
/**
* This method returns the principal[user-name] of logged-in user.
*/
private String getPrincipal(){
String userName = null;
Object principal = getCurrentUser();
if (principal instanceof UserDetails) {
userName = ((UserDetails)principal).getUsername();
} else {
userName = principal.toString();
}
return userName;
}
private Object getCurrentUser(){
return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
//The rest part of the controller}
I using TestNG based on the this tutorial: http://websystique.com/springmvc/spring-4-mvc-and-hibernate4-integration-testing-example-using-annotations/, and currently I have following in my test cases:
//all the import file
public class AppControllerTest {
#Mock
UserService userService;
#Mock
MessageSource message;
#InjectMocks
AppController appController;
#Spy
List<User> users = new ArrayList<User>();
#Spy
ModelMap model;
#Mock
BindingResult result;
#BeforeClass
public void setUp(){
MockitoAnnotations.initMocks(this);
users = getUsers();
}
private List<User> getUsers() {
// TODO Auto-generated method stub
User u1 = new User();
u1.setId(1);
u1.setFirstName("Admin");
u1.setLastName("Admin");
u1.setUsername("admin");
u1.setEmail("admin#akb.co.jp");
u1.setDateOfBirth(new LocalDate());
u1.setPassword("admin");
Department admin = new Department();
admin.setId(1);
admin.setName("Admin");
admin.setDescription("Admin");
u1.setDepartment(admin);
Role adminRole = new Role();
adminRole.setId(1);
adminRole.setRoleName("ADMIN");
Set<Role> roles = new HashSet<>();
roles.add(adminRole);
u1.setRoles(roles);
User u2 = new User();
u2.setId(1);
u2.setFirstName("Alice");
u2.setLastName("Lin");
u2.setUsername("alice.lin");
u2.setEmail("alice.lin#akb.co.jp");
u2.setDateOfBirth(new LocalDate());
u2.setPassword("Alice0102");
u2.setDepartment(admin);
u2.setRoles(roles);
users.add(u1);
users.add(u2);
return users;
}
#Test
public void listUsers(){
when(userService.findAllUsers()).thenReturn(users);
Assert.assertEquals(appController.listUsers(model), "userslist");
Assert.assertEquals(model.get("users"), users);
verify(userService, atLeastOnce()).findAllUsers();
}
}
Now the question is, if I didn't comment this line model.addAttribute("loggedinuser", getPrincipal());
in my controller class, when I run maven test, it will throw null pointer exception, that is obvious, since in my test cases I didn't login to the application. What can I do so I can make the test passed include this line?
You should refactor your code so that the getCurrentUser() calls live in a separate class. You should keep those separate anyway because most likely other controllers will need to make the same calls. But for this context, you need to refactor because you cannot mock private method calls (at least not using Mockito).
Once the user related calls are in a separate class, you can mock it just as you have done the other services above, using #Mock annotation.