I have a springboot test case trying to test a method which has some dependency on env properties.
#SpringBootTest(classes = ABCApi.class)
#AutoConfigureMockMvc
class sampleTest {
#Autowired
private WebApplicationContext context;
protected MockMvc mockMvc;
#BeforeEach
public void setup() {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
#InjectMocks
private EmployeeServiceImpl employeeServiceImpl;
#Test
void testEmployeeServiceImpl() {
String summary = "summary";
String description = "description";
String response = employeeServiceImpl.createIssue(summary, description);
verify(EmployeeService, times(1)).createIssue(summary, description);
}
}
Note: I don't need to test the response. All I need is to make sure that the method is called. The response will be null because of some unavailable parameters.
Below is the EmployeeServiceImpl code which has all properties listed. I have the application.yml file in src/test/resources, which is the right location. But while debugging the test case above, the below class is not loading any properties from application.yml sitting in test folder structure.
#Service
public class EmployeeServiceImpl {
#Value("${emp.username}")
private String username;
#Value("${emp.password}")
private String password;
#Value("${emp.url}")
private String url;
#Override
public String createIssue(String summary, String description) {
EmpRestClient client = setEmpClient();
EmpSuperClient empSuperClient = client.getProject();
IssueInput newIssue = buildNewIssueInput(description, summary);
return empSuperClient.createIssue(newIssue).claim().getKey();
}
private EmpRestClient setEmpClient() {
return new EmpRestClientFactory()
.createWithBasicHttpAuthentication(URI.create(url), username, password);
}
I know that these properties are not supposed to be in service layer and instead should be loaded as a bean in config class which will be a future improvement.
How do I fix that test case?
Use constructor injection to inject dependencies. It makes code easy to test.
private final String username;
private final String password;
private final String url;
public EmployeeServiceImpl (#Value("${emp.username}") final String username, #Value("${emp.password}") final String password,
#Value("${emp.url}") String url) {
this.username = username;
this.password = password;
this.url = url;
}
Now you can use the constructor to create EmployeeServiceImpl in your test class.
E.g.
new EmployeeServiceImpl ("username", "password", "http://localhost:8080")
Related
I'm trying to test a comment_post method.
Comment has many - to - one relationship with User Entity which comes from Spring Security.
I connected this relationship by using Principal.
I think I made it working properly, but having trouble applying it to test.
Problem is that Comment Posting method gets user by finding User in Repository using Principal's email attribute, So I need to apply SecurityContext to test,
but I have no idea how to apply this function to test.
By Searching, I found out that I can make SpringSecurityContext by #WithSecurityContext
annotation, so I'm trying to apply it but having this error
java.lang.RuntimeException: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springboot.web.CommentsApiControllerTest$WithUserDetailsSecurityContextFactory': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'springboot.web.CommentsApiControllerTest' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
I'm not even sure that my approach is correct.
tbh, I kind of feel lost, maybe it's because I'm new to SpringBoot, also Security.
Here's my codes.
CommentService
#RequiredArgsConstructor
#Service
public class CommentService {
private final CommentRepository commentRepository;
private final PostsRepository postsRepository;
private final UserDetailService userDetailService;
#Transactional
public Long commentSave(CommentSaveRequestDto requestDto, Long id) {
Posts post = postsRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("해당 게시글이 존재하지 않습니다"));
requestDto.setPosts(post);
User user = userDetailService.returnUser();
requestDto.setUser(user);
return commentRepository.save(requestDto.toEntity()).getId();
}
`
UserDetailService
#RequiredArgsConstructor
#Service
public class UserDetailService {
private final UserRepository userRepository;
public User returnUser() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String userName;
if (principal instanceof UserDetails) {
userName = ((UserDetails) principal).getUsername();
} else {
userName = principal.toString();
}
int start = userName.indexOf("email")+6;
int end = userName.indexOf(".com,")+4;
String email = userName.substring(start, end);
User user = userRepository.findByEmail(email).orElse(null);
return user;
}
CommentSaveRequestDto
#Data
#NoArgsConstructor
#Builder
#AllArgsConstructor
public class CommentSaveRequestDto {
private String comment;
private Posts posts;
private User user;
/* Dto -> Entity */
public Comment toEntity() {
return Comment.builder()
.comment(comment)
.posts(posts)
.user(user)
.build();
}
}
And here is my CommentsApiControllrTest
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#Transactional
public class CommentsApiControllerTest {
#LocalServerPort
private int port;
#Autowired
private PostsRepository postsRepository;
#Autowired
private CommentRepository commentRepository;
#Autowired
private UserRepository userRepository;
#Autowired
private PostsService postsService;
#Autowired
private CommentService commentService;
#Autowired
private UserDetailService userDetailsService;
#Autowired
private WebApplicationContext context;
#Autowired ObjectMapper objectMapper;
private MockMvc mvc;
#Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.apply(sharedHttpSession())
.build();
}
#Retention(RetentionPolicy.RUNTIME)
#WithSecurityContext(factory = WithUserDetailsSecurityContextFactory.class)
public #interface WithMockCustomUser {
String name() default "testName";
String email() default "testemail#gmail.com";
Role role() default Role.USER;
}
final class WithUserDetailsSecurityContextFactory implements WithSecurityContextFactory<WithUserDetails> {
private final UserDetailsService userDetailsService;
#Autowired
public WithUserDetailsSecurityContextFactory(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public org.springframework.security.core.context.SecurityContext createSecurityContext(WithUserDetails withUser) {
String username = withUser.value();
Assert.hasLength(username, "value() must be non-empty String");
UserDetails principal = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities());
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authentication);
return context;
}
}
#After
public void tearDown() throws Exception {
postsRepository.deleteAll();
commentRepository.deleteAll();
}
#Test
#WithMockCustomUser
#Transactional // 프록시 객체에 실제 데이터를 불러올 수 있게 영속성 컨텍스트에서 관리
public void comment_등록() throws Exception {
// given
String title = "title";
String content = "content";
User user = userRepository.save(User.builder()
.name("name")
.email("fake#naver.com")
.picture("fakePic.com")
.role(Role.USER)
.build());
PostsSaveRequestDto requestDto = PostsSaveRequestDto.builder()
.title(title)
.content(content)
.user(user)
.build();
postsRepository.save(requestDto.toEntity());
String comment = "comment";
Posts posts = postsRepository.findAll().get(0);
CommentSaveRequestDto saveRequestDto = CommentSaveRequestDto.builder()
.comment(comment)
.posts(posts)
.build();
Long id = posts.getId();
String url = "http://localhost:"+ port + "/api/posts/" + id + "/comments";
//when
mvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content(objectMapper.writeValueAsString(saveRequestDto)))
.andExpect(status().isOk())
.andDo(print());
}
All I want is to make a mock Security User in test, so that
User user = userDetailService.returnUser();
this line in CommentService don't make any error.
Just a little tip would be really helpful to me.
Thank you in advance.
I have exception-messages written down in the application.yml. They are pure text, which is later reformatted using java.text.MessageFormat.
I have got the following custom RuntimeException my service throws when login failed:
#Component
public class AccountLoginFailedException extends RuntimeException {
#Autowired
public AccountLoginFailedException(#Value("#(${authservice.exception-messages.login-failed})") final String message, #Qualifier(value = "Credentials") final Credentials credentials) {
super(MessageFormat.format(message, credentials.getUsername()));
}
}
My test, which solely tests the AccountController and mocks away the service behind it:
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = AuthServiceTestConfiguration.class)
#WebMvcTest(AccountController.class)
public class AccountControllerTest {
#Autowired
private BeanFactory beanFactory;
#Autowired
private ObjectMapper objectMapper;
#Autowired
private TestHelper helper;
#MockBean
private AccountService accountService;
#Autowired
private JwtService jwtService;
#Autowired
private MockMvc mvc;
#Test
public void test_LoginFailed_AccountDoesNotExist() throws Exception {
// Given
final Credentials credentials = helper.testCredentials();
final String credentialsJson = objectMapper.writeValueAsString(credentials);
final AccountLoginFailedException loginFailedException = beanFactory.getBean(AccountLoginFailedException.class, credentials);
// When
given(accountService.login(credentials)).willThrow(loginFailedException);
// Then
mvc
.perform(
post("/login")
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
.contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
.content(credentialsJson))
.andExpect(status().isUnprocessableEntity())
.andExpect(jsonPath("$.data").value(equalTo(loginFailedException.getMessage())));
}
}
message contains the correct String. However: credentials contains just an empty object (not null) instead of the one created using helper.testCredentials().
Here is a slightly simplified TestHelper class I am using:
#TestComponent
public class TestHelper {
public static final String USERNAME = "SomeUsername";
public static final String PASSWORD = "SomePassword";
#Autowired
private BeanFactory beanFactory;
public Credentials testCredentials() {
final Credentials credentials = beanFactory.getBean(Credentials.class.getSimpleName(), Credentials.class);
credentials.setUsername(USERNAME);
credentials.setPassword(PASSWORD);
return credentials;
}
}
These custom exceptions are thrown by my application only and are always expected to contain the credentials (username) responsible for it. I also have a AccountExceptionsControllerAdvice-class, which just wraps these custom exceptions in a generic JSON response, exposing the error in a preferred manner.
How can I ensure that this particular instance of Credentials is inserted into the particular instance of AccountLoginFailedException? Or should I not be autowiring exceptions at all?
You could mock your Credentials component in your tests as follows:
#MockBean
private Credentials credentials;
#Before
public void before() {
when(credentials.getUsername()).thenReturn(USERNAME);
when(credentials.getPassword()).thenReturn(PASSWORD);
}
I have a Spring Boot 2 web service. It accepts a Bearer token and uses Spring OAuth2 to go to an external URL and grab the users details using that token. I'm trying to create tests that test the implementation of the web service, but I'm struggling to figure out how to pass the AuthenticationPrinicple from a test?
Here is an example of a method in my controller;
#RequestMapping(value = "", method = RequestMethod.GET, produces = "application/json")
public ResponseEntity listUserUploadedFiles(#AuthenticationPrincipal Client client) {
FileListResponse rsp = new FileListResponse(fileStorageService.getUserFiles(client.getUid()));
return rsp.response(HttpStatus.OK);
}
Here is my test so far;
#Test
public void TestSimple() throws Exception {
mockMvc.perform(get("/file"))
.andExpect(status().isOk());
}
Currently the test fails because the Client (AuthenticationPrinicple) is null.
How would I do this?
---- UPDATE ----
Here is my Client object, as you can see, it doesn't inherit UserDetails.
public class Client implements Serializable {
private static final long serialVersionUID = 1L;
private String uid;
private String email;
public String getUid() {
return uid;
}
public void setUid(String uid) {
this.uid = uid;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
There is also a PrincipalExtractor bean setup.
#Bean
public PrincipalExtractor principalExtractor() {
return map -> {
Client client = new Client();
client.setUid(String.valueOf(map.get("uid")));
client.setEmail(String.valueOf(map.get("name")));
return client;
};
}
To provide a little more information on the project. This is a microservice, the user authenticates via a web application which takes place externally to this service. When calls to this server are made, a token is supplied which is verified with an authenticate server (again, external). The response from authentication server provides us with a uuid and an email field, the PrincipalExtractor maps these to a Client object, which is then passed into the controller to be used.
You could mock your user when testing #WithMockUser.
If you use MockMvc there is no extra sec-config needed
#RunWith(SpringRunner.class)
#WebMvcTest(SecuredController.class)
public class SecuredControllerWebMvcIntegrationTest {
#Autowired
private MockMvc mockMvc;
#Test
#WithMockUser(value = "user")
public void testSimple() throws Exception {
mockMvc.perform(get("/file"))
...
if you want to use #WithMockUser in a #SpringBootTest you need extra configuration
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SecuredControllerSpringBootIntegrationTest
...
#Autowired
private WebApplicationContext context;
private MockMvc mvc;
#Before
public void setup() {
mvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
}
...
ref spring-security-integration-tests
I don't know if this is a spring boot or gradle issue
I am using spring boot 1.4 to build Restful Services.
I have a StatusController class which gives status of service. This includes service name,buildtime,environmenet and version.
Here is my Controller
#RestController
public class StatusController
{
#Value("${service.version: not configured}")
private String version;
#Value("${service.name: not configured}")
private String appName;
#Value("${service.env: not configured}")
private String env;
#Value("${service.timestamp: not configured}")
private String buildDate;
#RequestMapping(value = "/status", method = RequestMethod.GET)
#ResponseStatus(HttpStatus.OK)
#ResponseBody
public String getStatus() throws JsonProcessingException {
Map<String,String> map = new HashMap<>();
map.put("version", version);
map.put("appName", appName);
map.put("env", env);
map.put("buildDate", buildDate);
return new ObjectMapper().writeValueAsString(map);
}
}
src/main/resources/application.properties
service.name=${name}
service.version=${version}
service.timestamp=${timestamp}
build.gradle (only relevant section)
processResources {
expand project.properties
}
def buildTime() {
def today = new Date()
def formattedDate = today.format('MM-dd-yyyy HH:mm:ss z')
return formattedDate
}
ext.timestamp=buildTime()
I have version defined in gradle.properties
version=1.0.0-SNAPSHOT.
when I start my service (gradlew bootRun) and I hit /status, I see all the values injected (not the default values of "not configured". However, in my tests, see the values not being substituted but default values injected.
Here is my test class
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ApplicationTests {
#Test
public void contextLoads() {
System.out.println("test passed");
}
#Value("${service.version: not configured}")
private String version;
#Value("${service.name: not configured}")
private String appName;
#Value("${service.env: not configured}")
private String env;
#Value("${service.timestamp: not configured}")
private String buildDate;
#Autowired
private TestRestTemplate restTemplate;
#Test
public void exampleTest() throws Exception {
String lifeCheck = this.restTemplate.getForObject("/status", String.class);
System.out.print(lifeCheck);
}
}
using debugger, I confirmed the values are not substituted in the test class.
but also not substituted in controller
Here is the output from running above test
{"appName":" not configured","buildDate":" not configured","env":" not configured","version":" not configured"}
Don't forget that there's a separate task in Gradle called processTestResources
Place the application.properties in your src/test/resources/
The test classes cannot see your main resources. The values will be substituted if you place it in your test resources folder. :)
I have the following code which attempts to save a POJO object (Actor) into MongoDB using Spring Mongo Repository, but the repository object is always Null. I have followed multiple examples but mainly this one
The POJO class:
#Document(collection = "actors")
public class Actor
{
#Id
private String id;
...
//constructor
//setters & getters
}
The repository:
public interface ActorRepository extends MongoRepository<Actor, String>
{
public Actor findByFNameAndLName(String fName, String lName);
public Actor findByFName (String fName);
public Actor findByLName(String lName);
}
The service that uses the repository:
#Service
public class ActorService
{
#Autowired
private ActorRepository actorRepository;
public Actor insert(Actor a)
{
a.setId(null);
return actorRepository.save(a);
}
}
And I access the service from a REST controller class:
#RestController
public class Controllers
{
private static final Logger logger = Logger.getLogger(Controllers.class);
private static final ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringMongoConfig.class);
private ActorService actorService = new ActorService();
#RequestMapping(value="/createActor", method=RequestMethod.POST)
public #ResponseBody String createActor(#RequestParam(value = "fName") String fName,
#RequestParam(value = "lName") String lName,
#RequestParam(value = "role") String role)
{
return actorService.insert(new Actor(null,fName,lName,role)).toString();
}
...
}
The error that I get is NullPointerException from this line: return actorRepository.save(a); in the ActorService.insert() method.
Any Idea why is this happening?
EDIT: Here is the Spring Configurations
#Configuration
public class SpringMongoConfig extends AbstractMongoConfiguration
{
#Bean
public GridFsTemplate gridFsTemplate() throws Exception
{
return new GridFsTemplate(mongoDbFactory(), mappingMongoConverter());
}
#Override
protected String getDatabaseName()
{
return "SEaaS";
}
#Override
#Bean
public Mongo mongo() throws Exception
{
return new MongoClient("localhost" , 27017 );
}
public #Bean MongoTemplate mongoTemplate() throws Exception
{
return new MongoTemplate(mongo(), getDatabaseName());
}
}
The problem is that you are not using Spring to get the ActorService dependency -instead you have manually instantiated the dependency using
private ActorService actorService = new ActorService();.
The following code is the easiest fix in order to inject the ActorService dependency into the controller.
#RestController
public class Controllers
{
private static final Logger logger = Logger.getLogger(Controllers.class);
private static final ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringMongoConfig.class);
#Autowired
private ActorService actorService;
#RequestMapping(value="/createActor", method=RequestMethod.POST)
public #ResponseBody String createActor(#RequestParam(value = "fName") String fName,
#RequestParam(value = "lName") String lName,
#RequestParam(value = "role") String role)
{
return actorService.insert(new Actor(null,fName,lName,role)).toString();
}
...
}