How do you pass program arguments in a #SpringBootTest? - java

I'm building a spring boot application. I want to run it like this:
java -jar myjar.jar inputFile outputFile
How do I write a #SpringBootTest for this? I imagine that using #SpringBootTest would make Spring fail during startup because some of my code would say, "you need to provide an inputFile and outputFile". Is there a way to pass program arguments when using a #SpringBootTest?
I'm inferring from this answer that I may have to use a SpringApplicationBuilder to do this.
I thought I had the answer but I was wrong. This incorrect information may still be useful to some:
(This information is wrong. I think that some arguments can't be referred to in code as properties, but not all. I still don't know how to get the application arguments in a #SpringBootTest) I was confused because I didn't understand the terminology. The annotation has a parameter for "properties". I thought it was to point it at a property file, but the documentation says:
Properties in form key=value that should be added to the Spring Environment before the test runs.
The other piece of the terminology puzzle is that what I called "program arguments" the Spring docs refer to as "properties".
This is some additional relevant documentation: https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#boot-features-application-arguments
This is a workaround (not an answer). You can do something like this:
private SpringApplicationBuilder subject;
#Before
public void setUp() throws Exception {
subject = new SpringApplicationBuilder(Application.class);
}
#Test
public void requiresInputAndOutput() throws Exception {
thrown.expect(IllegalStateException.class);
subject.run();
}
#Test
public void happyPathHasNoErrors() throws Exception {
subject.run(EXISTING_INPUT_FILE, "does_not_exist");
}
I don't like this very much. It prevents me from using #Autowired elsewhere in my test.

if your program arguments is
--arg1=val1
before springboot version 2.2.0
you can use
#SpringBootTest({"arg1=val1"})
after springboot 2.2.0
you can use
#SpringBootTest(args={"--arg1=val1"})

I had the same issue. Out of the box, the SpringBootContextLoader (used by default with #SpringBootTest) always runs your app without any arguments. However, you can provide your own bootstrapper, which can then in turn provide your own SpringApplication that gets called to run your test. From there, you can override the run(String... args) method to provide whatever arguments you want.
For example, given a simple application:
#SpringBootApplication
public class Main {
public static void main(final String[] args) {
new SpringApplicationBuilder(Main.class).run(args);
}
#Bean
public ApplicationRunner app() {
return args -> System.out.println(args.getOptionNames());
}
}
You can inject test arguments with:
#ContextConfiguration(classes = Main.class)
#ExtendWith(SpringExtension.class)
#BootstrapWith(RestartAppsTest.Bootstrapper.class)
class RestartAppsTest {
static class Bootstrapper extends SpringBootTestContextBootstrapper {
static class ArgumentSupplyingContextLoader extends SpringBootContextLoader {
#Override
protected SpringApplication getSpringApplication() {
return new SpringApplication() {
#Override
public ConfigurableApplicationContext run(String... args) {
return super.run("--restart");
}
};
}
}
#Override
protected Class<? extends ContextLoader> getDefaultContextLoaderClass(Class<?> testClass) {
return ArgumentSupplyingContextLoader.class;
}
}
#Test
void testRestart() {
//
}
}
It's obviously a bit verbose, but it works. You could clean it up and make a nicer/reusable bootstrapper that looked for your own annotation (or possibly reuse JUnit Jupiter's #Arguments) that declared what arguments to supply (instead of hardcoding them).

You can use #SpringBootTest(classes=Application.class, args ={inputFile, outputFile}) if your app's main method looks a little different like
public static void main(String[] args){
SpringApplication.run(Application.class, args);
}

For me working example is
#SpringBootTest(args ={"--env", "test"})
class QuickFixServerAppTest {
#Test
void loadContextTest() {
}
}
is the same as passing the
--env test
argument when starting Spring

Normally you're writing tests for your services; not the boot strapper. Spring Boot will pass command line parameters to your classes - perhaps using the #Value annotation, which in turn will be parameters to your service. Consider testing your services using the SpringRunner. Here's an example from my code base.
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = {
Neo4jConfigTest.class,
FdTemplate.class,
FdServerIo.class,
MapBasedStorageProxy.class})
#ActiveProfiles({"dev", "fd-auth-test", "fd-client"})
public class TestEntityLinks {
#Autowired
private ContentModelService contentModelService;
#Autowired
private BatchService batchService;
#Test
public void doSomething () { ... }

Related

getScreenshotAs is giving null exception on webDriver

I am using spring framework with selenium and testNG, then I decided to integrate with allure reports, the reports are working fine.
My issue is with attaching screenshots on failure.
The #Autowired driver is returning null when getScreenshotAs is called.
I'll post some code, if more is needed let me know.
#Component
public class ScreenshotUtil {
#Autowired
private WebDriver driver;
#Attachment(value = "Screenshot", type = "image/png")
public byte[] saveScreenshotOnFailure() {
return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); //this is where im getting error
}
}
Then I have the listener class..
public class AllureListener implements ITestListener {
#Autowired
private ScreenshotUtil screenshotUtil;
#Override
public void onTestFailure(ITestResult result) {
screenshotUtil.saveScreenshotOnFailure();
}
}
I have my test classes with #Test all working fine.. I'll add the WebDriverConfig component aswell (dont know if that will be useful).
#Configuration
public class WebDriverConfig {
#Bean
#Scope("browserscope") //This is for parallel run
public WebDriver driver() {
if(System.getProperty("browser").equals("firefox") {
.. return firefox driver //let me know if this code might be necessary
} else {
.. return chrome driver //let me know if this code might be necessary
}
}
#Bean
public WebDriverWait webDriverWait(WebDriver driver) {
return new WebDriverWait(driver, timeout);
}
}
Thanks in advance, any help is appreciated..
See if the #Component annotation will solve your issue, since you're using #Autowired on a property.
Decorate the driver() method in WebDriverConfig with #Component("driver"); and see if it works.
More details here - https://www.baeldung.com/spring-autowire
It would be better if you could put the entire stack trace.
It appears that the configuration class is not able to provide the driver bean.
You can try the following two things.
Try removing the #Scope (which would make the bean singleton that shouldn't be an issue here, you can use "prototype" scope )
Debug the code place debug point inside the Bean and see if the bean is getting returned.
I want to answer my own question, because after a lot of help and some research I was able to solve this issue.
What happens is, spring context is different from testNG context.
When we implement ITestListener class, the TestNG context does not have access to spring context. This class is necessary for taking screenshots on test failure.
So the solution is (I took it from here: https://dzone.com/articles/autowiring-spring-beans-into-classes-not-managed-by-spring)
First create this service to retrieve the beans from spring:
#Service
public class BeanUtil implements ApplicationContextAware {
private static ApplicationContext context;
#Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static <T> T getBean(Class<T> beanClass) {
return context.getBean(beanClass);
}
}
Then on my AllureListener class I'll retrieve the ScreenshotUtil bean like this:
public class AllureListener implements ITestListener {
#Override
public void onTestFailure(ITestResult result) {
ScreenshotUtil screenshotUtil = BeanUtil.getBean(ScreenshotUtil.class);
screenshotUtil.saveScreenshotOnFailure();
}
}
Hopefully this will help someone in the future using spring framework, spring and allure report.

How to reuse Testcontainers between multiple SpringBootTests?

I'm using TestContainers with Spring Boot to run unit tests for repositories like this:
#Testcontainers
#ExtendWith(SpringExtension.class)
#ActiveProfiles("itest")
#SpringBootTest(classes = RouteTestingCheapRouteDetector.class)
#ContextConfiguration(initializers = AlwaysFailingRouteRepositoryShould.Initializer.class)
#TestExecutionListeners(listeners = DependencyInjectionTestExecutionListener.class)
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
#Tag("docker")
#Tag("database")
class AlwaysFailingRouteRepositoryShould {
#SuppressWarnings("rawtypes")
#Container
private static final PostgreSQLContainer database =
new PostgreSQLContainer("postgres:9.6")
.withDatabaseName("database")
.withUsername("postgres")
.withPassword("postgres");
But now I have 14 of these tests and every time a test is run a new instance of Postgres is spun up. Is it possible to reuse the same instance across all tests? The Singleton pattern doesn't help since every test starts a new application.
I've also tried testcontainers.reuse.enable=true in .testcontainers.properties and .withReuse(true), but that didn't help.
You can't use the JUnit Jupiter annotation #Container if you want to have reusable containers. This annotation ensures to stop the container after each test.
What you need is the singleton container approach, and use e.g. #BeforeAll to start your containers. Even though you then have .start() in multiple tests, Testcontainers won't start a new container if you opted-in for reusability using both .withReuse(true) on your container definition AND the following .testcontainers.properties file in your home directory:
testcontainers.reuse.enable=true
A simple example might look like the following:
#SpringBootTest
public class SomeIT {
public static GenericContainer postgreSQLContainer = new PostgreSQLContainer().
withReuse(true);
#BeforeAll
public static void beforeAll() {
postgreSQLContainer.start();
}
#Test
public void test() {
}
}
and another integration test:
#SpringBootTest
public class SecondIT {
public static GenericContainer postgreSQLContainer = new PostgreSQLContainer().
withReuse(true);
#BeforeAll
public static void beforeAll() {
postgreSQLContainer.start();
}
#Test
public void secondTest() {
}
}
There is currently a PR that adds documentation about this
I've put together a blog post explaining how to reuse containers with Testcontainers in detail.
If you decide go forward with the singleton pattern, mind the warning in "Database containers launched via JDBC URL scheme". I took hours till I note that, even though I was using the singleton pattern, an additional container was always being created mapped on a different port.
In summary, do not use the test containers JDBC (host-less) URIs, such as jdbc:tc:postgresql:<image-tag>:///<databasename>, if you need use the singleton pattern.
Accepted answer is great but the problem is you still have to repeat the configurations(creating, starting and etc.) for each integration tests. It would be better to have simpler configuration with fewer lines of code. I think cleaner version would be using JUnit 5 extensions.
This is how I solved the problem. Below sample uses MariaDB container but the concept is applicable to all.
Create the container config holding class:
public class AppMariaDBContainer extends MariaDBContainer<AppMariaDBContainer> {
private static final String IMAGE_VERSION = "mariadb:10.5";
private static final String DATABASE_NAME = "my-db";
private static final String USERNAME = "user";
private static final String PASSWORD = "strong-password";
public static AppMariaDBContainer container = new AppMariaDBContainer()
.withDatabaseName(DATABASE_NAME)
.withUsername(USERNAME)
.withPassword(PASSWORD);
public AppMariaDBContainer() {
super(IMAGE_VERSION);
}
}
Create an extension class that starts the container and sets the DataSource properties. And run migrations if needed:
public class DatabaseSetupExtension implements BeforeAllCallback {
#Override
public void beforeAll(ExtensionContext context) {
AppMariaDBContainer.container.start();
updateDataSourceProps(AppMariaDBContainer.container);
//migration logic here (if needed)
}
private void updateDataSourceProps(AppMariaDBContainer container) {
System.setProperty("spring.datasource.url", container.getJdbcUrl());
System.setProperty("spring.datasource.username", container.getUsername());
System.setProperty("spring.datasource.password", container.getPassword());
}
}
Add #ExtendWith to your test class
#SpringBootTest
#ExtendWith(MariaDBSetupExtension.class)
class ApplicationIntegrationTests {
#Test
void someTest() {
}
}
Another test
#SpringBootTest
#ExtendWith(MariaDBSetupExtension.class)
class AnotherIntegrationTests {
#Test
void anotherTest() {
}
}
Using either singleton containers or reusable containers are possible solutions but because they don't scope the life-cycle of the container to that of the application context both are less then ideal.
It is however possible to scope the container to the application contexts lifecycle by using a ContextCustomizerFactory and I've written about this in more detail in a blog post.
In a test use:
#Slf4j
#SpringBootTest
#EnabledPostgresTestContainer
class DemoApplicationTest {
#Test
void contextLoads() {
log.info("Hello world");
}
}
Then enable the annotation in META-INF/spring.factories:
org.springframework.test.context.ContextCustomizerFactory=\
com.logarithmicwhale.demo.EnablePostgresTestContainerContextCustomizerFactory
Which can be implemented as:
public class EnablePostgresTestContainerContextCustomizerFactory implements ContextCustomizerFactory {
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Inherited
public #interface EnabledPostgresTestContainer {
}
#Override
public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
if (!(AnnotatedElementUtils.hasAnnotation(testClass, EnabledPostgresTestContainer.class))) {
return null;
}
return new PostgresTestContainerContextCustomizer();
}
#EqualsAndHashCode // See ContextCustomizer java doc
private static class PostgresTestContainerContextCustomizer implements ContextCustomizer {
private static final DockerImageName image = DockerImageName
.parse("postgres")
.withTag("14.1");
#Override
public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) {
var postgresContainer = new PostgreSQLContainer<>(image);
postgresContainer.start();
var properties = Map.<String, Object>of(
"spring.datasource.url", postgresContainer.getJdbcUrl(),
"spring.datasource.username", postgresContainer.getUsername(),
"spring.datasource.password", postgresContainer.getPassword(),
// Prevent any in memory db from replacing the data source
// See #AutoConfigureTestDatabase
"spring.test.database.replace", "NONE"
);
var propertySource = new MapPropertySource("PostgresContainer Test Properties", properties);
context.getEnvironment().getPropertySources().addFirst(propertySource);
}
}
}
I'm not sure how #Testcontainers works, but I suspect it might work per class.
Just make your singleton static as described in Singleton pattern
and get it in every test from your signleton holder, don't define it in every test class.

Spring Boot CommandLineRunner : filter option argument

Considering a Spring Boot CommandLineRunner Application, I would like to know how to filter the "switch" options passed to Spring Boot as externalized configuration.
For example, with:
#Component
public class FileProcessingCommandLine implements CommandLineRunner {
#Override
public void run(String... strings) throws Exception {
for (String filename: strings) {
File file = new File(filename);
service.doSomething(file);
}
}
}
I can call java -jar myJar.jar /tmp/file1 /tmp/file2 and the service will be called for both files.
But if I add a Spring parameter, like java -jar myJar.jar /tmp/file1 /tmp/file2 --spring.config.name=myproject then the configuration name is updated (right!) but the service is also called for file ./--spring.config.name=myproject which of course doesn't exist.
I know I can filter manually on the filename with something like
if (!filename.startsWith("--")) ...
But as all of this components came from Spring, I wonder if there is not a option somewhere to let it manage it, and to ensure the strings parameter passed to the run method will not contain at all the properties options already parsed at the Application level.
Thanks to #AndyWilkinson enhancement report, ApplicationRunner interface was added in Spring Boot 1.3.0 (still in Milestones at the moment, but will soon be released I hope)
Here the way to use it and solve the issue:
#Component
public class FileProcessingCommandLine implements ApplicationRunner {
#Override
public void run(ApplicationArguments applicationArguments) throws Exception {
for (String filename : applicationArguments.getNonOptionArgs())
File file = new File(filename);
service.doSomething(file);
}
}
}
There's no support for this in Spring Boot at the moment. I've opened an enhancement issue so that we can consider it for a future release.
One option is to use Commons CLI in the run() of your CommandLineRunner impl.
There is a related question that you may be interested.
Here is another solution :
#Component
public class FileProcessingCommandLine implements CommandLineRunner {
#Autowired
private ApplicationConfig config;
#Override
public void run(String... strings) throws Exception {
for (String filename: config.getFiles()) {
File file = new File(filename);
service.doSomething(file);
}
}
}
#Configuration
#EnableConfigurationProperties
public class ApplicationConfig {
private String[] files;
public String[] getFiles() {
return files;
}
public void setFiles(String[] files) {
this.files = files;
}
}
Then run the program :
java -jar myJar.jar --files=/tmp/file1,/tmp/file2 --spring.config.name=myproject

Why Spring force me to use static variable?

I am still a beginner in Spring . I have one test class as below an run by TestNG ...
#Service("springTest")
public class SpringTest {
private MyService myService;
#Autowired
public void setMyService(MyService myService) {
this.myService = myService;
// below was ok , nothing error
System.out.println(myService.getClass());
}
//org.testng.annotations.Test
#Test
public void doSomethingTest() {
load();
// That may cause NullPointerException
myService.doSomething();
}
private void load(){
// here codes for load spring configuration files
}
}
So , I have no idea why myService.doSomething(); produce NullPointerException ? Can someone give me suggestions what I need to do ? What problem may cause this error ? What am I wrong ? Now I am using as ...
#Service("springTest")
public class SpringTest {
private static MyService myService;
#Autowired
public void setMyService(MyService myService) {
SpringTest.myService = myService;
System.out.println(myService.getClass());
}
//org.testng.annotations.Test
#Test
public void doSomethingTest() {
load();
// fine , without error
myService.doSomething();
}
private void load(){
// here codes for load spring configuration files
}
}
PS: I think I don't need to show my spring configuration file , because it is really simple and I believe that will not be cause any error. So , I left it to describe. But if you want to see , I can show you. And then please assume my spring configurartion file was loaded properly and this was loaded before my Test classes run.
Thanks for reading my question.By the way, I can't also use field
injection and I can only use setter method injection.
Just remove the static initializer. Keep setter injection (I prefer it).
Use your first example, the second code is wrong - don't use it.
Make sure you tests are being run by spring (and therefore the bean is initialized correctly by spring).
The null pointer is caused by running the test method before spring has initialized the bean.
You need somethign like this
#RunWith (SpringJUnit4ClassRunner.class)
#ContextConfiguration (locations = "classpath:/config/applicationContext-test.xml")
public class SpringTest {...}

spring3 annotation with main method

I have following class:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(locations={"classpath:/com/home/app/Home-ctx.xml"})
public class LoginDetailsTest {
public static void main(String[] args) {
new LoginDetailsTest().testLoginDetails();
}
#Inject
#Named(HomeConstants.loginDetailsService)
private LoginDetailsService loginDetailsService;
private List<UserLogin> loginDetails;
#Test
public void testLoginDetails() {
UserLogin login = new UserLogin();
login.setLoginName("test");
login.setLoginPassword("test123");
loginDetails = loginDetailsService.loginDetails(login);
for (UserLogin loginDet : loginDetails) {
System.out.println(loginDet.getLoginName());
System.out.println(loginDet.getLoginPassword());
}
}
}
if i run above code as junit test, then it gives expected result.
If I run as Java application ie main method, then it gives null pointer exception for
loginDetailsService.loginDetails(login). how can run as main method without error?
You still need to do what JUnit does when you "run code as junit test" to bootstrap your application context and dependency injection:
public static void main(String[] args) {
org.junit.runner.JUnitCore.run(LoginDetailsTest.class);
}
The mainis a different thing. Because by instantiating the class by new LoginDetailsTest() it is not build by Spring - there can be no dependency injection.
What you need to do is:
make a new application context appctx.xml for your main method that imports Home-ctx.xml and declare a new bean <bean id="loginDetailsTest" class="LoginDetailsTest"/>
in your main method get an instance of the bean and call testLoginDetails() like this:
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("appctx.xml");
LoginDetailsTest loginDetailsTest = (LoginDetailsTest) context.getBean("loginDetailsTest");
loginDetailsTest.testLoginDetails();
}
In general you should separate the JUnit test, main method and business logic.
First things first the reason your test works is (SpringJUnit4ClassRunner), It does a lot but to keep it simple it boot straps the spring container and injects all the dependencies like that you defined in your context file (Home-ctx.xml) including the one you inject into the test case. For more details look at these classes
http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/test/context/TestContextManager.html
http://static.springsource.org/spring/docs/2.5.x/api/org/springframework/test/context/support/DependencyInjectionTestExecutionListener.html
To solve the problem with your main method, You have to load the spring context your self and inject the dependency some thing like this
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("/com/home/app/Home-ctx.xml");
new LoginDetailsTest().loginDetailsService = (LoginDetailsService) ctx.getBean(LoginDetailsService.class);
//now your main method should work
new LoginDetailsTest().testLoginDetails();

Categories