-
Notifications
You must be signed in to change notification settings - Fork 10
Testing Java Spring applications
In our Java testing library there is a possibility to test web backend applications based on Spring library.
If you want to develop code problems that are meant to be tested via Hyperskill's web interface then you should treat every stage as a separate individual code problem.
The most convenient way to write Spring tests is to use dynamic tests. However, you're not going to start any programs, Spring server will be running all the time while all tests are executed. Dynamic tests will allow you to execute requests in a way that each request will be treated as a separate test.
We'll start from the end of the regular Java project setup.
Your setup so far should look like this:
Every Spring application should contain its own dependencies in a separate build.gradle
file at the root of every stage.
Here its possible content. Notice the usage of apply plugin: 'hyperskill'
and Spring Boot dependencies not hardcoded, but actually hidden behind variables hs.spring.bootVersion
and hs.spring.dependencyManagementVersion
. They are defined in hs-gradle-plugin repo and are updated there so that we don't have to update every Spring Boot project/problem on Hyperskill separately in case we want to update dependencies.
It's strongly recommended to use the following template and just add to it adidtional dependencies. If you need other things in your build.gradle
, please contact Hyperskill.
buildscript {
apply plugin: 'hyperskill'
repositories {
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:$hs.spring.bootVersion"
classpath "io.spring.gradle:dependency-management-plugin:$hs.spring.dependencyManagementVersion"
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
repositories {
mavenCentral()
}
sourceSets.main.resources.srcDirs = ["src/resources"]
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
}
The following dependencies are installed for web testing Spring applications on Hyperskill. You may use all of them or part of them, but no more if you are writing tests to be graded in web interface. Contact Hyperksill so we can add other useful dependencies:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-freemarker'
}
Also, you should create a resources/application.properties
file and set up a port 28852
and open external shutdown possibility via actuator, so tests can shut down Spring application externally. Use the following lines (you can add other lines, but these ones should be here):
server.port=28852
management.endpoints.web.exposure.include=*
management.endpoint.shutdown.enabled=true
Now your setup may look like follows:
If you see the addition (excluded)
near your files, you should right-click on the file and select Course Creator -> Include into Task
Note: On Hyperskill's web interface, only a single file can be shown, so you should mark files that users shouldn't change as hidden. Normally, in a regular project, only tests are hidden but keeping in mind that users should solve this problem via web interface you should leave only one file as visible. So, tests, build.gradle
, application.properties
and all except one Java sources should be hidden. You can hide files right-clicking and selecting Course Creator -> Hide from Learner
After all, to publish problems on Cogniterra do a right-click on the icon with 4 empty squares and select Course Creator -> Upload Hyperskill Lesson on Cogniterra
Let's test some class named SpringDemoApplication
.
First of all, you need to extend SpringTest
class and pass this class to the constructor along with the port number on which you are going to run and test this Spring application.
import org.hyperskill.hstest.stage.SpringTest;
import springdemo.SpringDemoApplication;
public class DemoTest extends SpringTest {
public DemoTest() {
super(SpringDemoApplication.class, 28852);
}
}
You can omit the port, this way the library will scan the properties file for the port number.
super(SpringDemoApplication.class);
If you want to use a database, you can pass the name of the database as the second parameter.
super(SpringDemoApplication.class, "../demodb.mv.db");
You can pass all 3 parameters:
super(SpringDemoApplication.class, 28852, "../demodb.mv.db");
Provided the following lines in the properties:
spring.datasource.url=jdbc:h2:file:../demodb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false
And in the build.gradle
dependencies {
// ...
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// ...
}
Now, let's imagine that on URL /api/user/
the Spring application should return a list of all users in the JSON format (in this case, there should be 2 users in the output). The test will look like this:
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import org.hyperskill.hstest.dynamic.input.DynamicTestingMethod;
import org.hyperskill.hstest.mocks.web.response.HttpResponse;
import org.hyperskill.hstest.stage.SpringTest;
import org.hyperskill.hstest.testcase.CheckResult;
import springdemo.SpringDemoApplication;
...
CheckResult testGet() {
HttpResponse response = get("/api/user/").send();
if (response.getStatusCode() != 200) {
return CheckResult.wrong("GET /api/user/ should respond with " +
"status code 200, responded: " + response.getStatusCode() + "\n\n" +
"Response body:\n" + response.getContent());
}
JsonElement json;
try {
json = response.getJson();
} catch (Exception ex) {
return CheckResult.wrong("GET /api/user/ should return a valid JSON");
}
if (!json.isJsonArray()) {
return CheckResult.wrong("GET /api/user/ should return an array of objects");
}
JsonArray array = json.getAsJsonArray();
if (array.size() != 2) {
return CheckResult.wrong("GET /api/user/ should initially return 2 objects");
}
return CheckResult.correct();
}
Note: for complex JSON responses you may want to use advanced JSON checking.
Notice, that you shouldn't add annotation @DynamicTestingMethod
, you will add it later.
Notice the method get
- you can use it to send a GET request to the tested application. You also can use post
, put
, and delete
methods - they all declared in the SpringTest
class. They are:
-
HttpRequest get(String address)
constructs a GET request to the specified address. Address should not contain host or port - they are added automatically. The address should already contain GET parameters in case you want to add them to the request. -
HttpRequest post(String address, String content)
constructs a POST request with JSON content and also adds headerContent: application/json
-
HttpRequest post(String address, Map<String, String> params)
constructs a POST request with params content and also adds headerContent: application/x-www-form-urlencoded
-
HttpRequest put(String address, String content)
constructs a PUT request with JSON content and also adds headerContent: application/json
-
HttpRequest put(String address, Map<String, String> params)
constructs a PUT request with params content and also adds headerContent: application/x-www-form-urlencoded
-
HttpRequest delete(String address)
constructs a DELETE request to the specified address
You can see an example with the post
method below (for example, an application should create a new user using POST /api/user/
and return a total number of users):
private String newUser = """
{
"id": 8,
"name": "NEW-USER",
"email": "[email protected]",
"skills": [
{
"name": "JAVA"
},
{
"name": "KOTLIN"
}
]
}""";
...
CheckResult testPost(int requiredUsers) {
HttpResponse response = post("/api/user/", newUser).send();
if (response.getStatusCode() != 200) {
return CheckResult.wrong("POST /api/user/ should respond with " +
"status code 200, responded: " + response.getStatusCode() + "\n\n" +
"Response body:\n" + response.getContent());
}
int totalUsers;
try {
totalUsers = Integer.parseInt(response.getContent());
} catch (NumberFormatException ex) {
return CheckResult.wrong("POST /api/user/ " +
"should create a user and return a number of users. No number was in the response.");
}
if (totalUsers != requiredUsers) {
return CheckResult.wrong("POST /api/user/ " +
"should create a user and return a number of users. " +
"Expected to receive \"" + requiredUsers + "\", received: \"" + totalUsers + "\"");
}
return CheckResult.correct();
}
Notice, that you can use Java 16's Text Blocks because we use Java 17 in tests. It's really useful to store JSON using Text Blocks.
You can define a series of requests as your test. This way, every request would be treated as an individual test. Mark with @DynamicTest
annotation a public List
or public array of DynamicTesting
objects. See the real example below form the WebQuizEngine
project:
@DynamicTest
DynamicTesting[] dt = new DynamicTesting[] {
() -> testAllQuizzes(0),
() -> testCreateQuiz(1),
() -> testQuizExists(1),
() -> testQuizNotExists(1),
() -> testAllQuizzes(2),
this::reloadServer,
() -> testAllQuizzes(2),
() -> checkQuizSuccess(quizIds[0], "[2]", true),
() -> checkQuizSuccess(quizIds[0], "[3]", false),
() -> checkQuizSuccess(quizIds[1], "[0]", false),
() -> checkQuizSuccess(quizIds[1], "[1]", true),
() -> addIncorrectQuiz(error400noTitle),
() -> addIncorrectQuiz(error400emptyTitle),
() -> addIncorrectQuiz(error400noText),
() -> addIncorrectQuiz(error400emptyText),
() -> addIncorrectQuiz(error400noOptions),
() -> addIncorrectQuiz(error400emptyOptions),
() -> addIncorrectQuiz(error400oneOption),
() -> testCreateQuiz(3),
() -> testQuizExists(3),
() -> testQuizNotExists(3),
() -> checkQuizSuccess(quizIds[3], "[]", false),
() -> checkQuizSuccess(quizIds[3], "[0]", false),
() -> checkQuizSuccess(quizIds[3], "[1]", false),
() -> checkQuizSuccess(quizIds[3], "[2]", false),
() -> checkQuizSuccess(quizIds[3], "[3]", false),
() -> testAllQuizzes(7),
this::reloadServer,
() -> testAllQuizzes(7),
() -> checkQuizSuccess(quizIds[5], "[]", true),
() -> checkQuizSuccess(quizIds[5], "[0]", false),
() -> checkQuizSuccess(quizIds[6], "[0,1,2]", false),
() -> checkQuizSuccess(quizIds[6], "[0,1,3]", true),
};
You can see the this::reloadServer
line, it's used to check if the user used the database to store data and not in-memory objects (in case it is mentioned in the description that the database should be used, otherwise no need to reload the server). Typical relaodServer
method is presented below (it didn't implemented in SpringTest
class):
private CheckResult reloadServer() {
try {
reloadSpring();
} catch (Exception ex) {
throw new UnexpectedError(ex.getMessage());
}
return CheckResult.correct();
}
- Home
- About
- Initial setup
- Writing tests
- Guidelines for writing tests
- Outcomes of testing
- Generating and checking
- Presentation error
- Checking JSON
- Testing solutions written in different languages
- Creating Hyperskill problems based on hs-test
- Testing Java Swing applications
- Testing Java Spring applications
- Testing Ktor applications