ECE366 - Lesson 12

Testing, Software Engineering Ethics

Instructor: Professor Hong

Review & Questions

## Java Testing
## 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
## JUnit and Maven - Create a new Maven project - Add Maven dependency ``` <dependencies> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.9.2</version> <scope>test</scope> </dependency> </dependencies> ```
## Your First Test src/main/java/Code.java ``` public class Code { public String sayHello() { return "Hello world!"; } } ```
## Your First Test test/java/CodeTest.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()); } } ```
## Basic Tests
## Bank Account src/java/BankAccount.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 ``` 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 ``` 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 Properties for BankAccount src/java/BankAccount.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 ``` 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 afer 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 ``` 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 ``` 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 ``` 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()); } } } ```
## More Advanced Tests
## 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 ``` 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 ``` 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 ``` 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 ``` 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 ``` 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 ``` junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.config.strategy=dynamic ```
## Parallel Execution tests/java/BankAccountParallelExecutionTest.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 ``` 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 ``` 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_16}) public void testJRE() { } @Test @DisabledOnJre({JRE.JAVA_16}) public void testNoJRE16() { } } ``` Generally not good practice
## Disable a Test tests/java/BankAccountDisabledTest.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"); } } ```
## Mockito - A way to mock functions/systems pom.xml ``` <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>2.23.4</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.8.0</version> <scope>test</scope> </dependency> ```
## Calculate Methods src/main/java/CalculateMethods.java ``` public class CalculateMethods { public double divide(int x, int y) { return x / y; } } ```
## Mockito Test test/java/CalculateMethodsMockitoTest.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)); } } ```
## Running Reports
## Surefire Reports mvn.pom ``` <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.2</version> </plugin> <plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>2.22.2</version> </plugin> </plugins> </build> ``` ``` mvn surefire-report:report ```
## Jacoco pom.xml ``` <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.9</version> <executions> <execution> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>prepare-package</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> ``` ``` mvn jacoco:report ```
## 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
## Software Engineering Ethics
## Software Engineering Ethics [https://ethics.acm.org/code-of-ethics/software-engineering-code/](https://ethics.acm.org/code-of-ethics/software-engineering-code/)
## Final Project Requirement - Take 10 items from the software engineering ethics and describe how you followed them in your project - 2.5 points / 25 points