diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5eac309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2ca15ec..65724af 100644 --- a/pom.xml +++ b/pom.xml @@ -7,6 +7,37 @@ ch.engenius accounts 1.0-SNAPSHOT - + + + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + 11 + + + + + + + org.junit.jupiter + junit-jupiter-api + 5.8.2 + test + + + org.mockito + mockito-core + 4.0.0 + test + + + org.mockito + mockito-junit-jupiter + 4.0.0 + test + + \ No newline at end of file diff --git a/src/main/java/ch/engenius/bank/Account.java b/src/main/java/ch/engenius/bank/Account.java deleted file mode 100644 index b9979cb..0000000 --- a/src/main/java/ch/engenius/bank/Account.java +++ /dev/null @@ -1,31 +0,0 @@ -package ch.engenius.bank; - -import java.math.BigDecimal; - -public class Account { - private double money; - - public void withdraw(double amount) { - if ((money - amount) < 0) { - throw new IllegalStateException("not enough credits on account"); - } - setMoney(money - amount); - - } - - public void deposit(double amount) { - setMoney(money + amount); - } - - public double getMoney() { - return money; - } - - public void setMoney(double money) { - this.money = money; - } - - public BigDecimal getMoneyAsBigDecimal() { - return BigDecimal.valueOf(money); - } -} diff --git a/src/main/java/ch/engenius/bank/Bank.java b/src/main/java/ch/engenius/bank/Bank.java deleted file mode 100644 index 571ebc7..0000000 --- a/src/main/java/ch/engenius/bank/Bank.java +++ /dev/null @@ -1,18 +0,0 @@ -package ch.engenius.bank; - -import java.util.HashMap; - -public class Bank { - private HashMap accounts = new HashMap<>(); - - public Account registerAccount(int accountNumber, int amount) { - Account account = new Account(); - account.setMoney(amount); - accounts.put(accountNumber, account); - return account; - } - - public Account getAccount( int number) { - return accounts.get(number); - } -} diff --git a/src/main/java/ch/engenius/bank/BankRunner.java b/src/main/java/ch/engenius/bank/BankRunner.java index 10b30fd..73eb8fe 100644 --- a/src/main/java/ch/engenius/bank/BankRunner.java +++ b/src/main/java/ch/engenius/bank/BankRunner.java @@ -1,10 +1,12 @@ package ch.engenius.bank; +import ch.engenius.bank.domain.Account; +import ch.engenius.bank.service.BankService; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; import java.util.Random; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; import java.util.stream.IntStream; public class BankRunner { @@ -12,60 +14,63 @@ public class BankRunner { private static final ExecutorService executor = Executors.newFixedThreadPool(8); private final Random random = new Random(43); - private final Bank bank = new Bank(); + private final BankService bankService = new BankService(); public static void main(String[] args) { BankRunner runner = new BankRunner(); int accounts = 100; - int defaultDeposit = 1000; + BigDecimal defaultDeposit = BigDecimal.valueOf(1000); int iterations = 10000; - runner.registerAccounts(accounts, defaultDeposit); - runner.sanityCheck(accounts, accounts*defaultDeposit); - runner.runBank(iterations, accounts); - runner.sanityCheck(accounts, accounts*defaultDeposit); - + try { + runner.registerAccounts(accounts, defaultDeposit); + runner.sanityCheck(accounts, defaultDeposit.multiply(BigDecimal.valueOf(accounts))); + runner.runBank(iterations, accounts); + runner.sanityCheck(accounts, defaultDeposit.multiply(BigDecimal.valueOf(accounts))); + } catch (IllegalStateException e) { + System.out.println(e.getMessage()); + } } private void runBank(int iterations, int maxAccount) { - for (int i =0; i< iterations; i++ ) { - executor.submit( ()-> runRandomOperation(maxAccount)); + List> callableTasks = new ArrayList<>(); + + for (int i = 0; i < iterations; i++) { + Callable callableTask = () -> runRandomOperation(maxAccount); + callableTasks.add(callableTask); } + try { - executor.shutdown(); - executor.awaitTermination(100,TimeUnit.SECONDS); + executor.invokeAll(callableTasks); } catch (InterruptedException e) { e.printStackTrace(); } + executor.shutdown(); } - private void runRandomOperation(int maxAccount) { - double transfer = random.nextDouble()*100.0; + private Void runRandomOperation(int maxAccount) { + BigDecimal transfer = BigDecimal.valueOf(random.nextDouble() * 100.0); int accountInNumber = random.nextInt(maxAccount); int accountOutNumber = random.nextInt(maxAccount); - Account accIn =bank.getAccount(accountInNumber); - Account accOut =bank.getAccount(accountOutNumber); - accIn.deposit(transfer); - accOut.withdraw(transfer); + bankService.transferMoney(transfer, accountInNumber, accountOutNumber); + return null; } - private void registerAccounts(int number, int defaultMoney) { - for ( int i = 0; i < number; i++) { - bank.registerAccount(i, defaultMoney); + private void registerAccounts(int number, BigDecimal defaultMoney) { + for (int i = 0; i < number; i++) { + bankService.registerAccount(i, defaultMoney); } } - private void sanityCheck( int accountMaxNumber, int totalExpectedMoney) { + private void sanityCheck(int accountMaxNumber, BigDecimal totalExpectedMoney) { BigDecimal sum = IntStream.range(0, accountMaxNumber) - .mapToObj( bank::getAccount) - .map ( Account::getMoneyAsBigDecimal) - .reduce( BigDecimal.ZERO, BigDecimal::add); + .mapToObj(bankService::getAccount) + .map (Account::getMoney) + .reduce(BigDecimal.ZERO, BigDecimal::add); - if ( sum.intValue() != totalExpectedMoney) { - throw new IllegalStateException("we got "+ sum + " != " + totalExpectedMoney +" (expected)"); + if (sum.compareTo(totalExpectedMoney) != 0) { + throw new IllegalStateException("We got "+ sum + " != " + totalExpectedMoney +" (expected)"); } - System.out.println("sanity check OK"); + System.out.println("Sanity check OK"); } - - } diff --git a/src/main/java/ch/engenius/bank/domain/Account.java b/src/main/java/ch/engenius/bank/domain/Account.java new file mode 100644 index 0000000..0031a92 --- /dev/null +++ b/src/main/java/ch/engenius/bank/domain/Account.java @@ -0,0 +1,62 @@ +package ch.engenius.bank.domain; + +import java.math.BigDecimal; +import java.util.concurrent.locks.ReentrantLock; + +public class Account { + + private BigDecimal money; + private ReentrantLock lock; + + public Account() { + this.money = BigDecimal.ZERO; + this.lock = new ReentrantLock(); + } + + public Account(BigDecimal money) { + this.lock = new ReentrantLock(); + try { + validAmount(money); + this.money = money; + } catch (IllegalArgumentException e) { + this.money = BigDecimal.ZERO; + System.out.println(e.getMessage()); + } + } + + public BigDecimal getMoney() { + return money; + } + + public void withdraw(BigDecimal amount) { + lock.lock(); + try { + validAmount(amount); + if (money.subtract(amount).compareTo(BigDecimal.ZERO) < 0) + throw new IllegalStateException("Not enough credits on account"); + + money = money.subtract(amount); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } finally { + lock.unlock(); + } + } + + public void deposit(BigDecimal amount) { + lock.lock(); + try { + validAmount(amount); + money = money.add(amount); + } catch (IllegalArgumentException e) { + System.out.println(e.getMessage()); + } finally { + lock.unlock(); + } + } + + private void validAmount(BigDecimal amount) { + if (amount.compareTo(BigDecimal.ZERO) <= 0) + throw new IllegalArgumentException("Amount should have positive value"); + } +} diff --git a/src/main/java/ch/engenius/bank/domain/Bank.java b/src/main/java/ch/engenius/bank/domain/Bank.java new file mode 100644 index 0000000..5c9266c --- /dev/null +++ b/src/main/java/ch/engenius/bank/domain/Bank.java @@ -0,0 +1,16 @@ +package ch.engenius.bank.domain; + +import java.util.HashMap; + +public class Bank { + + private HashMap accounts; + + public Bank() { + this.accounts = new HashMap<>(); + } + + public HashMap getAccounts() { + return accounts; + } +} diff --git a/src/main/java/ch/engenius/bank/service/BankService.java b/src/main/java/ch/engenius/bank/service/BankService.java new file mode 100644 index 0000000..db77f91 --- /dev/null +++ b/src/main/java/ch/engenius/bank/service/BankService.java @@ -0,0 +1,40 @@ +package ch.engenius.bank.service; + +import ch.engenius.bank.domain.Account; +import ch.engenius.bank.domain.Bank; +import java.math.BigDecimal; +import java.util.NoSuchElementException; + +public class BankService { + + private Bank bank; + + public BankService() { + this.bank = new Bank(); + } + + public Account registerAccount(int accountNumber, BigDecimal amount) { + Account account = new Account(amount); + bank.getAccounts().put(accountNumber, account); + return account; + } + + public Account getAccount(int number) { + if (!bank.getAccounts().containsKey(number)) + throw new NoSuchElementException("Account with number: " + number + " does not exist"); + + return bank.getAccounts().get(number); + } + + public void transferMoney(BigDecimal money, int accountInNumber, int accountOutNumber) { + try { + Account accountIn = getAccount(accountInNumber); + Account accountOut = getAccount(accountOutNumber); + + accountOut.withdraw(money); + accountIn.deposit(money); + } catch (NoSuchElementException e) { + System.out.println(e.getMessage()); + } + } +} diff --git a/src/test/java/ch/engenius/bank/domain/AccountTest.java b/src/test/java/ch/engenius/bank/domain/AccountTest.java new file mode 100644 index 0000000..1e5b1b7 --- /dev/null +++ b/src/test/java/ch/engenius/bank/domain/AccountTest.java @@ -0,0 +1,41 @@ +package ch.engenius.bank.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.math.BigDecimal; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AccountTest { + + private Account account; + + @BeforeEach + public void setUp() { + account = new Account(BigDecimal.valueOf(1000)); + } + + @Test + public void shouldCreateAccount() { + assertEquals(BigDecimal.valueOf(1000), account.getMoney()); + } + + @Test + public void shouldAddMoney() { + account.deposit(BigDecimal.valueOf(100)); + assertEquals(BigDecimal.valueOf(1100), account.getMoney()); + } + + @Test + public void shouldSubtractMoney() { + account.withdraw(BigDecimal.valueOf(100)); + assertEquals(BigDecimal.valueOf(900), account.getMoney()); + } + + @Test + public void shouldThrowException_whenThereIsNotEnoughMoney() { + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> account.withdraw(BigDecimal.valueOf(2000))); + assertEquals("Not enough credits on account", exception.getMessage()); + assertEquals(BigDecimal.valueOf(1000), account.getMoney()); + } +} diff --git a/src/test/java/ch/engenius/bank/service/BankServiceIntTest.java b/src/test/java/ch/engenius/bank/service/BankServiceIntTest.java new file mode 100644 index 0000000..5febd41 --- /dev/null +++ b/src/test/java/ch/engenius/bank/service/BankServiceIntTest.java @@ -0,0 +1,59 @@ +package ch.engenius.bank.service; + +import ch.engenius.bank.domain.Account; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import java.math.BigDecimal; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class BankServiceIntTest { + + private BankService bankService; + + @BeforeAll + public void setUp() { + bankService = new BankService(); + } + + @Test + public void shouldTransferMoney() { + Account accountIn = bankService.registerAccount(1, BigDecimal.valueOf(1000)); + Account accountOut = bankService.registerAccount(2, BigDecimal.valueOf(2000)); + + bankService.transferMoney(BigDecimal.valueOf(100), 1, 2); + assertEquals(BigDecimal.valueOf(1900), accountOut.getMoney()); + assertEquals(BigDecimal.valueOf(1100), accountIn.getMoney()); + } + + @Test + public void shouldCreateAccountWithZeroAmount_whenAmountIsNegative() { + Account account = bankService.registerAccount(1, BigDecimal.valueOf(-1000)); + assertEquals(BigDecimal.ZERO, account.getMoney()); + } + + @Test + public void shouldThrowException_whenThereIsNotEnoughMoneyOnAccount() { + Account accountIn = bankService.registerAccount(1, BigDecimal.valueOf(1000)); + Account accountOut = bankService.registerAccount(2, BigDecimal.valueOf(2000)); + + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { + bankService.transferMoney(BigDecimal.valueOf(2500), 1, 2); + }); + assertEquals("Not enough credits on account", exception.getMessage()); + assertEquals(BigDecimal.valueOf(1000), accountIn.getMoney()); + assertEquals(BigDecimal.valueOf(2000), accountOut.getMoney()); + } + + @Test + public void shouldNotTransferMoney_whenTransferAmountIsNegative() { + Account accountIn = bankService.registerAccount(1, BigDecimal.valueOf(1000)); + Account accountOut = bankService.registerAccount(2, BigDecimal.valueOf(2000)); + + bankService.transferMoney(BigDecimal.valueOf(-100), 1, 2); + assertEquals(BigDecimal.valueOf(1000), accountIn.getMoney()); + assertEquals(BigDecimal.valueOf(2000), accountOut.getMoney()); + } +} diff --git a/src/test/java/ch/engenius/bank/service/BankServiceTest.java b/src/test/java/ch/engenius/bank/service/BankServiceTest.java new file mode 100644 index 0000000..10218c8 --- /dev/null +++ b/src/test/java/ch/engenius/bank/service/BankServiceTest.java @@ -0,0 +1,59 @@ +package ch.engenius.bank.service; + +import ch.engenius.bank.domain.Account; +import ch.engenius.bank.domain.Bank; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import java.math.BigDecimal; +import java.util.HashMap; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +public class BankServiceTest { + + @Mock + private HashMap accounts; + + @Mock + private Account account; + + @Mock + private Bank bank; + + @InjectMocks + private BankService bankService = new BankService(); + + @Test + public void shouldReturnAccountByProvidedNumber() { + Mockito.when(bank.getAccounts()).thenReturn(accounts); + Mockito.when(accounts.containsKey(1)).thenReturn(true); + Mockito.when(accounts.get(1)).thenReturn(account); + + assertEquals(account, bankService.getAccount(1)); + } + + @Test + public void shouldTestTransferMoney() { + Account accountIn = Mockito.mock(Account.class); + Account accountOut = Mockito.mock(Account.class); + + Mockito.when(bank.getAccounts()).thenReturn(accounts); + Mockito.when(accounts.containsKey(1)).thenReturn(true); + Mockito.when(accounts.containsKey(2)).thenReturn(true); + Mockito.when(accounts.get(1)).thenReturn(accountIn); + Mockito.when(accounts.get(2)).thenReturn(accountOut); + + Mockito.doNothing().when(accountOut).withdraw(BigDecimal.valueOf(100)); + Mockito.doNothing().when(accountIn).deposit(BigDecimal.valueOf(100)); + + bankService.transferMoney(BigDecimal.valueOf(100), 1, 2); + verify(accountOut, times(1)).withdraw(BigDecimal.valueOf(100)); + verify(accountIn, times(1)).deposit(BigDecimal.valueOf(100)); + } +}