ECE366 - Lesson 13
Review - Hosting on the Cloud and Testing
Instructor: Professor Hong
## Your First Azure Container
## Installing CLI
- [https://learn.microsoft.com/en-us/cli/azure/install-azure-cli](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli)
- Azure Login:
- WSL: ```az login --use-device-code```
- Mac: ```az login```
## Your First Container
- [https://learn.microsoft.com/en-us/azure/container-instances/container-instances-quickstart](https://learn.microsoft.com/en-us/azure/container-instances/container-instances-quickstart)
```
az group create --name myResourceGroup --location eastus
az container create --resource-group myResourceGroup --name mycontainer --image mcr.microsoft.com/azuredocs/aci-helloworld --dns-name-label aci-demo324 --ports 80 --os-type Linux --cpu 1 --memory 1.5
az container show --resource-group myResourceGroup --name mycontainer --query "{FQDN:ipAddress.fqdn,ProvisioningState:provisioningState}" --out table
```
- Run the site provided
- You will have to update your dns label name
## Creating and Deploying a Container Image
## Creating and Deploying a Container Image
[https://learn.microsoft.com/en-us/azure/container-instances/container-instances-tutorial-prepare-app](https://learn.microsoft.com/en-us/azure/container-instances/container-instances-tutorial-prepare-app)
```
az acr create --resource-group myResourceGroup --name yourACRName --sku Basic
az acr update -n yourACRName --admin-enabled true
az container create --resource-group myResourceGroup --name aci-tutorial-app --image yourACRName.azurecr.io/aci-tutorial-app:v1 --cpu 1 --memory 1 --registry-login-server yourACRName.azurecr.io --registry-username yourACRName --registry-password $(az acr credential show --name yourACRName --query passwords[0].value -o tsv) --ip-address Public --dns-name-label yourDNSLabel --ports 80 --os-type Linux
```
- Replace `yourACRName` with a globally unique name (alphanumeric only)
- Replace `yourDNSLabel` with a unique DNS label
## Create a New React App
```
npm create vite@latest myapp -- --template react
cd myapp
npm install
npm run dev
```
## Dockerfile
```
FROM node:22-alpine AS build
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
RUN npm install -g serve
WORKDIR /usr/src/app
COPY --from=build /usr/src/app/dist ./dist
EXPOSE 80
CMD ["serve", "-s", "dist", "-l", "80"]
```
## Docker-compose.yaml
```
services:
ui:
build: .
image: samplereactacr.azurecr.io/ui
container_name: samplereact
ports:
- "80:80"
```
```
docker compose build
docker compose up
```
Note: We're using port 80
## Create Azure Parts
- Create an Azure Resource Group
- Create an Azure Container Registry
- Create an Azure Container Instance docker context
```
az login --use-device-code
az acr login --name samplereactacr
```
## Push to Azure and Run Container Instances
```
docker-compose push
```
## Deploying with a deploy-aci.yaml file
```
apiVersion: 2019-12-01
location: eastus
name: myContainerGroup5
properties:
containers:
- name: ui
properties:
image: myacr.azurecr.io/prepreact:latest
resources:
requests:
cpu: 1
memoryInGb: 1.5
ports:
- port: 80
- port: 8080
osType: Linux
ipAddress:
type: Public
dnsNameLabel: myappdnslabel3233 # Add your desired DNS name label here
ports:
- protocol: tcp
port: 80
- protocol: tcp
port: 8080
imageRegistryCredentials:
- server: myacr.azurecr.io
username: 00000000-0000-0000-0000-000000000000
password: yourpassword
tags: {exampleTag: tutorial}
type: Microsoft.ContainerInstance/containerGroups
```
```
az container create --resource-group myResourceGroup --file deploy-aci-react.yaml
```
## Deploying a Multi-Container Group
[https://learn.microsoft.com/en-us/azure/container-instances/container-instances-multi-container-yaml](https://learn.microsoft.com/en-us/azure/container-instances/container-instances-multi-container-yaml)
```
apiVersion: 2019-12-01
location: eastus
name: myContainerGroup3
properties:
containers:
- name: aci-tutorial-app
properties:
image: myacr.azurecr.io/aci-tutorial-app:v1
resources:
requests:
cpu: 1
memoryInGb: 1.5
ports:
- port: 80
- port: 8080
osType: Linux
ipAddress:
type: Public
dnsNameLabel: myappdnslabel323 # Add your desired DNS name label here
ports:
- protocol: tcp
port: 80
- protocol: tcp
port: 8080
imageRegistryCredentials:
- server: myacr.azurecr.io
username: 00000000-0000-0000-0000-000000000000
password: yourtoken
tags: {exampleTag: tutorial}
type: Microsoft.ContainerInstance/containerGroups
```
## Deploying a Multi-Container Group
```
apiVersion: 2019-12-01
location: eastus
name: myContainerGroup4
properties:
containers:
- name: aci-tutorial-app
properties:
image: myacr.azurecr.io/aci-tutorial-app:v1
resources:
requests:
cpu: 1
memoryInGb: 1.5
ports:
- port: 80
- port: 8080
- name: ui
properties:
image: myacr.azurecr.io/ui:latest
resources:
requests:
cpu: 1
memoryInGb: 1.5
osType: Linux
ipAddress:
type: Public
dnsNameLabel: myappdnslabel3232 # Add your desired DNS name label here
ports:
- protocol: tcp
port: 80
- protocol: tcp
port: 8080
imageRegistryCredentials:
- server: myacr.azurecr.io
username: 00000000-0000-0000-0000-000000000000
password: yourpassword
tags: {exampleTag: tutorial}
type: Microsoft.ContainerInstance/containerGroups
```
## Database
```
docker compose build
docker tag postgres:latest myacr.azurecr.io/postgres:latest
docker push myacr.azurecr.io/postgres:latest
```
```
apiVersion: 2019-12-01
location: eastus
name: postgresContainerGroup
properties:
containers:
- name: db
properties:
image: myacr.azurecr.io/postgres:latest
environmentVariables:
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_PASSWORD
value: password
resources:
requests:
cpu: 1
memoryInGb: 1.5
ports:
- port: 5432
osType: Linux
ipAddress:
type: Public
dnsNameLabel: postgresdnslabel # Add your desired DNS name label here
ports:
- protocol: tcp
port: 5432
imageRegistryCredentials:
- server: myacr.azurecr.io
username: 00000000-0000-0000-0000-000000000000
password: yourpassword
tags: {exampleTag: tutorial}
type: Microsoft.ContainerInstance/containerGroups
```
```
az container create --resource-group myResourceGroup --file deploy-aci.yaml
```
## Check db with dbeaver
- Check dbeaver
- host: postgresdnslabel.eastus.azurecontainer.io
- port: 5432
- database: rps
- username/password: postgres/password
## UI Docker File Update
```
# Stage 1: Build the React application
FROM node:22-alpine AS build
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: Serve with nginx + reverse proxy
FROM nginx:alpine
# Copy built static files
COPY --from=build /usr/src/app/dist /usr/share/nginx/html
# Copy nginx config with /api proxy
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```
## nginx.conf
```
server {
listen 80;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
## Adding the spring boot layer and react UI
docker-compose.yaml
```
services:
db:
image: myacr.azurecr.io/postgres:latest
build: db
environment:
- POSTGRES_DB=postgres
- POSTGRES_PASSWORD=password
expose:
- "5432"
ports:
- "5432:5432"
restart: always
app:
image: myacr.azurecr.io/springboot-app:latest
build: svc/rpsjpa
environment:
- POSTGRES_HOST=db
- POSTGRES_DB=rps
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
expose:
- "8080"
ports:
- "8080:8080"
depends_on:
- db
ui:
image: myacr.azurecr.io/rpsui:latest
build: ui
ports:
- "80:80"
depends_on:
- app
```
## Building and Pushing
```
docker compose build
docker compose push
```
## Updating the deploy-aci.yaml
```
apiVersion: 2019-12-01
location: eastus
name: fullStackContainerGroup
properties:
containers:
- name: db
properties:
image: myacr.azurecr.io/postgres:latest
environmentVariables:
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_PASSWORD
value: password
- name: POSTGRES_DB
value: postgres
resources:
requests:
cpu: 1
memoryInGb: 1.5
ports:
- port: 5432
- name: springboot-app
properties:
image: myacr.azurecr.io/springboot-app:latest
environmentVariables:
- name: POSTGRES_DB
value: postgres
- name: POSTGRES_PASSWORD
value: password
resources:
requests:
cpu: 1
memoryInGb: 1.5
ports:
- port: 8080
- name: ui
properties:
image: myacr.azurecr.io/rpsui:latest
resources:
requests:
cpu: 1
memoryInGb: 1.5
ports:
- port: 80
osType: Linux
ipAddress:
type: Public
dnsNameLabel: fullStackDns # Add your desired DNS name label here
ports:
- protocol: tcp
port: 5432
- protocol: tcp
port: 8080
- protocol: tcp
port: 80
imageRegistryCredentials:
- server: myacr.azurecr.io
username: 00000000-0000-0000-0000-000000000000
password: yourpassword
tags: {exampleTag: tutorial}
type: Microsoft.ContainerInstance/containerGroups
```
- Update IP address of server
## Deploy to Azure
```
az container create --resource-group myResourceGroup --file deploy-aci.yaml
```
## Make a basic spring boot project
- [https://start.spring.io/](https://start.spring.io/)
## Add Sample Class
```java
package com.example.demo;
public class Calculator {
public int multiplyByTwo(int x) {
return 2*x;
}
}
```
## Add Sample Tests
```java
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
class DemoApplicationTests {
@Test
void contextLoads() {
}
@Test
void testMultiplyByTwo() {
Calculator c = new Calculator();
int result = c.multiplyByTwo(2);
assertEquals(4, result);
}
}
```
## Testing Resources
[https://medium.com/capital-one-tech/improve-java-code-with-unit-tests-and-jacoco-b342643736ed](https://medium.com/capital-one-tech/improve-java-code-with-unit-tests-and-jacoco-b342643736ed)
[https://www.jetbrains.com/help/idea/running-test-with-coverage.html#run-config-with-coverage](https://www.jetbrains.com/help/idea/running-test-with-coverage.html#run-config-with-coverage)
## Types of Testing
- Unit testing - smallest pieces possible
- System testing - bigger part, complete API
- Integration testing - connecting different systems
- Acceptance testing - end user testing
- Performance testing - speed of program
- Regression testing - testing everything again
- Security testing - spot weaknesses in security
- Load testing - large loads / lots of hits
- End-to-end testing - entire system
## TDD
- Test Driven Development
- Written before implementation
- Tests will fail first and then succeed after writing code
- Forces developer to think about requirements
- Not skipped due to time pressure
- Bugs caught early
- Still need system and integration tests
## Advantages of Unit Testing
- Validating smallest units of software
- Find bugs easily and early
- Save time and money in the long run
- Forcing developers to write better and cleaner code
## JUnit
- Unit testing framework for Java
- Part of xUnit series
- Enables automated unit testing
## Your First Test
src/main/java/Code.java
```java
public class Code {
public String sayHello() {
return "Hello world!";
}
}
```
## Your First Test
test/java/CodeTest.java
```java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CodeTest {
@Test
public void testSayHello() {
Code code = new Code();
assertEquals("Hello world!", code.sayHello());
}
}
```
## Bank Account
src/java/BankAccount.java
```java
public class BankAccount {
private double balance;
private double minimumBalance;
public BankAccount(double balance, double minimumBalance) {
this.balance = balance;
this.minimumBalance = minimumBalance;
}
public double getBalance() {
return balance;
}
public double getMinimumBalance() {
return minimumBalance;
}
public double withdraw(double amount) {
if(balance - amount > minimumBalance) {
balance -= amount;
return amount;
} else {
throw new RuntimeException();
}
}
public double deposit(double amount) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return balance += amount;
}
}
```
## Bank Account Tests
test/java/BankAccountTest.java
```java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
@DisplayName("Test BankAccount class")
public class BankAccountTest {
@Test
@DisplayName("Withdraw 500 successfully")
public void testWithdraw() {
BankAccount bankAccount = new BankAccount(500, -1000);
bankAccount.withdraw(300);
assertEquals(200, bankAccount.getBalance());
}
@Test
@DisplayName("Deposit 500 successfully")
public void testDeposit() {
BankAccount bankAccount = new BankAccount(400, 0);
bankAccount.deposit(500);
assertEquals(900, bankAccount.getBalance(), "Unexpected value, expected 900");
}
}
```
- DisplayName gives the tests a better name/description
## Assertions
- Check the outcome of the test
- If the assertion fails, the test fails
- Assertions class in the org.junit.jupiter.api package
- Many methods and overrides
- Add isActive and holderName
## Additional Properties for BankAccount
src/java/BankAccount.java
```java
private boolean isActive = true;
private String holderName;
public boolean isActive() {
return isActive;
}
public void setActive(boolean active) {
isActive = active;
}
public String getHolderName() {
return holderName;
}
public void setHolderName(String holderName) {
this.holderName = holderName;
}
```
## Additional BankAccount Tests
tests/java/BankAccountAssertionTest.java
```java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.time.Duration;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertTimeout;
public class BankAccountAssertionsTest {
@Test
@DisplayName("Withdraw will become negative")
public void testWithdrawNotStuckAtZero() {
BankAccount bankAccount = new BankAccount(500, -1000);
bankAccount.withdraw(800);
assertNotEquals(0, bankAccount.getBalance());
}
@Test
@DisplayName("Test activation account after creation")
public void testActive() {
BankAccount bankAccount = new BankAccount(500, 0);
assertTrue(bankAccount.isActive());
}
@Test
@DisplayName("Test set holder name")
public void testHolderNameSet() {
BankAccount bankAccount = new BankAccount(500, 0);
bankAccount.setHolderName("Chris");
assertNotNull(bankAccount.getHolderName());
}
@Test
@DisplayName("Test that we can't withdraw below the minimum")
public void testNoWithdrawBelowMinimum() {
BankAccount bankAccount = new BankAccount(500, -1000);
assertThrows(RuntimeException.class, () -> bankAccount.withdraw(2000));
}
@Test
@DisplayName("Test no exceptions for withdraw and deposit")
public void testWithdrawAndDepositWithoutException() {
BankAccount bankAccount = new BankAccount(500, -1000);
assertAll(() -> bankAccount.deposit(200), () -> bankAccount.withdraw(450));
}
@Test
@DisplayName("Test speed deposit")
public void testDepositTimeout() {
BankAccount bankAccount = new BankAccount(400, 0);
assertTimeout(Duration.ofSeconds(1), () -> bankAccount.deposit(200));
}
// assertEquals, delta, message
// fail(); // fails the test
}
```
## Assumptions
- Setting a condition for executing a test
- If the assumption is met, the test will be executed
- If the assumption is not met, the test won't be executed
- Assumptions are in the org.junit.jupiter.api package
## Assumptions Test
tests/java/BankAccountAssumptionsTest.java
```java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.*;
public class BankAccountAssumptionsTest {
@Test
@DisplayName("Test activation account after creation")
public void testActive() {
BankAccount bankAccount = new BankAccount(500, 0);
assumeTrue(bankAccount != null);
assumeFalse(bankAccount == null);
assumingThat(bankAccount == null, () -> assertTrue(bankAccount.isActive()));
// doesn't abort the test and continues
assertTrue(bankAccount.isActive());
}
}
```
## Test Order
- Without specifying, we cannot predict the order of the tests
- Usually, this isn't a problem as the tests should be independent
- If you do need to order your tests, you can leverage the Order annotation
## Ordered Execution Tests
tests/java/BankAccountOrderedExecutionTest.java
```java
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import static org.junit.jupiter.api.Assertions.assertEquals;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BankAccountOrderedExecutionTest {
static BankAccount bankAccount = new BankAccount(0,0);
@Test
@Order(2)
public void testWithdraw() {
bankAccount.withdraw(300);
assertEquals(200, bankAccount.getBalance());
}
@Test
@Order(1)
public void testDeposit() {
bankAccount.deposit(500);
assertEquals(500, bankAccount.getBalance());
}
}
```
## Nested Tests
- Used to control the relationship between tests
- Useful when you have feature separation or when code is organized around a method or feature
## Nested Tests
tests/java/BankAccountNestedTest.java
```java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class BankAccountNestedTest {
@Test
@DisplayName("Withdraw 500 successfully")
public void testWithdraw() {
BankAccount bankAccount = new BankAccount(500, -1000);
bankAccount.withdraw(300);
assertEquals(200, bankAccount.getBalance());
}
@Test
@DisplayName("Deposit 400 successfully")
public void testDeposit() {
BankAccount bankAccount = new BankAccount(400, 0);
bankAccount.deposit(500);
assertEquals(900, bankAccount.getBalance());
}
@Nested
class WhenBalanceEqualsZero {
@Test
@DisplayName("Withdrawing below minimum balance: exception")
public void testWithdrawMinimumBalanceIs0() {
BankAccount bankAccount = new BankAccount(0, 0);
assertThrows(RuntimeException.class, () -> bankAccount.withdraw(500));
}
@Test
@DisplayName("Wtihdrawing below minimum balance: negative balance")
public void testWithdrawMinimumBalanceNegative1000() {
BankAccount bankAccount = new BankAccount(0, -1000);
bankAccount.withdraw(500);
assertEquals(-500, bankAccount.getBalance());
}
}
}
```
## Dependency Injection
- Less tightly coupled classes
- No need to manually create instances everytime
- No need to create new BankAccount(0,0) any more
## Parameter Resolver
tests/java/BankAccountParameterResolver.java
```java
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
public class BankAccountParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType() == BankAccount.class;
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
return new BankAccount(0, 0);
}
}
```
## Dependency Injection Test
tests/java/BankAccountDITest.java
```java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(BankAccountParameterResolver.class)
public class BankAccountDITest {
@Test
@DisplayName("Deposit 500 successfully")
public void testDeposit(BankAccount bankAccount) {
bankAccount.deposit(500);
assertEquals(500, bankAccount.getBalance());
}
}
```
## Repeated Tests
tests/java/BankAccountRepeatedTest.java
```java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(BankAccountParameterResolver.class)
public class BankAccountRepeatedTest {
@RepeatedTest(5)
@DisplayName("Deposit 400 successfully")
public void testDeposit(BankAccount bankAccount) {
bankAccount.deposit(500);
assertEquals(500, bankAccount.getBalance());
}
@RepeatedTest(5)
@DisplayName("Deposit 400 successfully")
public void testDepositRepetitionInfo(BankAccount bankAccount, RepetitionInfo repetitionInfo) {
bankAccount.deposit(500);
assertEquals(500, bankAccount.getBalance());
System.out.println("Nr: " + repetitionInfo.getCurrentRepetition());
}
}
```
Great for repeating tests multiple times.
## Parameterized Tests
tests/java/BankAccountParameterizedTest.java
```java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.time.DayOfWeek;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@ExtendWith(BankAccountParameterResolver.class)
public class BankAccountParameterizedTest {
@ParameterizedTest
@ValueSource(ints = {100, 400, 800, 1000})
@DisplayName("Deposit successfully")
public void testDeposit(int amount, BankAccount bankAccount) {
bankAccount.deposit(amount);
assertEquals(amount, bankAccount.getBalance());
}
@ParameterizedTest
@EnumSource(value = DayOfWeek.class, names = {"TUESDAY", "THURSDAY"})
public void testDayOfWeek(DayOfWeek day) {
assertTrue(day.toString().startsWith("T"));
}
@ParameterizedTest
@CsvSource({"100, Mary", "200, Richard", "150, Ted"})
//@CsvFileSource(resources = "details.csv", delimiter = ',')
public void depositAndNameTest(double amount, String name, BankAccount bankAccount) {
bankAccount.deposit(amount);
bankAccount.setHolderName(name);
assertEquals(amount, bankAccount.getBalance());
assertEquals(name, bankAccount.getHolderName());
}
}
```
- Run test several times with different parameters
## Timeout Tests
tests/java/BankAccountTimeoutTest.java
```java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.extension.ExtendWith;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTimeout;
// @Timeout(value=500, unit=imeUnit.MILLISECONDS)
@ExtendWith(BankAccountParameterResolver.class)
public class BankAccountTimeoutTest {
@Test
@Timeout(value=500, unit= TimeUnit.MILLISECONDS)
public void testDepositTimeoutAssertion(BankAccount bankAccount) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
bankAccount.deposit(300);
assertEquals(300, bankAccount.getBalance());
}
@Test
public void testDepositTimeoutAnnotation(BankAccount bankAccount) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bankAccount.deposit(300);
assertTimeout(Duration.ofMillis(500), () -> {
Thread.sleep(10);
});
}
}
```
## Parallel Execution Setup
tests/resources/junit-platform.properties
```properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.config.strategy=dynamic
```
## Parallel Execution
tests/java/BankAccountParallelExecutionTest.java
```java
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Execution(ExecutionMode.CONCURRENT)
@ExtendWith(BankAccountParameterResolver.class)
public class BankAccountParallelExecutionTest {
@Test
@DisplayName("Deposit 500 successfully")
public void testDeposit(BankAccount bankAccount) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
bankAccount.deposit(500);
assertEquals(500, bankAccount.getBalance());
}
@Test
@DisplayName("Deposit 500 successfully")
public void testDeposit2(BankAccount bankAccount) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bankAccount.deposit(500);
assertEquals(500, bankAccount.getBalance());
}
@Test
@DisplayName("Deposit 500 successfully")
public void testDeposit3(BankAccount bankAccount) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bankAccount.deposit(500);
assertEquals(500, bankAccount.getBalance());
}
}
```
## Before and After Test
tests/java/BankAccountBeforeAndAfterTest.java
```java
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // can use this for BeforeAll/AfterAll w/o static
public class BankAccountBeforeAndAfterTest {
static BankAccount bankAccount;
@BeforeAll
//@BeforeEach // fn must not be static
public void prepTest() {
System.out.println("Hi");
bankAccount = new BankAccount(500, 0);
}
@Test
public void testWithdraw() {
bankAccount.withdraw(300);
assertEquals(200, bankAccount.getBalance());
}
@Test
public void testDeposit() {
bankAccount.deposit(500);
assertEquals(1000, bankAccount.getBalance());
}
@AfterAll
//@AfterEach // fn must not be static
public void endTest() {
System.out.println("Bye");
}
}
```
## Conditional Tests
tests/java/BankAccountConditionalTest.java
```java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.*;
public class BankAccountConditionalTest {
@Test
@EnabledOnOs({OS.MAC})
public void testMac() {
}
@Test
@EnabledOnOs({OS.WINDOWS})
public void testWindows() {
}
@Test
@EnabledOnJre({JRE.JAVA_21})
public void testJRE() {
}
@Test
@DisabledOnJre({JRE.JAVA_21})
public void testNoJRE21() {
}
}
```
Generally not good practice
## Disable a Test
tests/java/BankAccountDisabledTest.java
```java
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(BankAccountParameterResolver.class)
public class BankAccountDisabledTest {
@Test
@Disabled("Temporarily disabled due to maintenance")
@DisplayName("Deposit 500 successfully")
public void testDeposit() {
BankAccount bankAccount = new BankAccount(400, 0);
bankAccount.deposit(500);
assertEquals(900, bankAccount.getBalance(), "Unexpected value, expected 900");
}
}
```
## Calculate Methods
src/main/java/CalculateMethods.java
```java
public class CalculateMethods {
public double divide(int x, int y) {
return x / y;
}
}
```
## Mockito Test
test/java/CalculateMethodsMockitoTest.java
```java
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(MockitoExtension.class)
public class CalculateMethodsMockitoTest {
@Mock
CalculateMethods calculateMethods;
@BeforeEach
public void setupMocks() {
Mockito.when(calculateMethods.divide(6,3)).thenReturn(2.0);
}
@Test
public void testDivide() {
assertEquals(2.0, calculateMethods.divide(6,3));
}
}
```
## Best Practices
- Keep it simple
- Test the actual unit of code
- Clear naming and stick to naming conventions
- Low cyclomatic complexity - the fewer codes in path, the better
- Shouldn't have implementation in code
- Deterministic tests - you should always have the same result