์ฌ์ฉ์๊ฐ ์์ฑํ ์์ฝ์๋ฅผ ์ค๋งํธ ์ปจํธ๋ํธ๋ก ๋ฐฐ๋ก ํ๋ ค๊ณ ํ๋๋ฐ, ์ด์๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ง๋ค์ด์ง ์์ฝ์๋ฅผ DB์ ๋ฃ์ ๋ค ์ค๋งํธ ์ปจํธ๋ํธ๋ก ํธ๋์ญ์ ์ ๋ง๋ค์ด ๋ฑ๋กํ๋ ๊ณผ์ ์์ 15์ด๋ ๊ฑธ๋ฆฌ๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค.
'์, ์ด ์์ ๋๋ฌด ์ค๋ ๊ฑธ๋ฆฌ๋๋ฐ... ์ฌ์ฉ์ ๊ธฐ๋ค๋ฆฌ๊ฒ ํ ์ ์์ง!๐ค' ๋ผ๋ ์๊ฐ์ผ๋ก ์ฌ์ฉ์๊ฐ ์ํํ๊ฒ ์๋น์ค๋ฅผ ์ด์ฉํ ์ ์๋๋ก ๋น๋๊ธฐ๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
๊ทธ๋์ ์ค๋์ Spring Boot์ ์ ์ฉํ ๊ธฐ๋ฅ
@Async
๋ฅผ ์ฌ์ฉํด์ ์๊ฐ์ด ์ค๋ ๊ฑธ๋ฆฌ๋ ์์ ์ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌํ๋ ๊ฒฝํ๊ณผ, ๊ทธ ๊ณผ์ ์์ ํ์ฉํ๋ CompletableFuture, Mockito ํ ์คํธ ๊ฒฝํ๐ , ๊ทธ๋ฆฌ๊ณ ๊ฐ์ฅ ์ค์ํ๋ @Transactional๊ณผ์ ๊ด๊ณ๐จ๊น์ง ์์ธํ ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํฉ๋๋ค.
๊ฐ์ฅ ๋จผ์ , Spring Boot์ "๋น๋๊ธฐ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๊ฒ ๋ค"๊ณ ์๋ ค์ฃผ์ด์ผ ํฉ๋๋ค. ๋ฉ์ธ ํด๋์ค๋ @Configuration ํด๋์ค์ @EnableAsync๋ฅผ ์ถ๊ฐํ๋ฉด ์ค๋น๊ฐ ๋๋ฉ๋๋ค.
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync // ์ถ๊ฐ!
public class MyAwesomeApplication {
// ... main ...
}
๊ทธ๋ฆฌ๊ณ ๋น๋๊ธฐ๋ก ์คํํ๊ณ ์ถ์ ๋ฉ์๋์ @Async๋ง ๋ถ์ฌ์ฃผ๋ฉด ๋ฉ๋๋ค. ๋งค์ฐ ๊ฐ๋จํฉ๋๋ค. (๋จ, public ๋ฉ์๋์ฌ์ผ ํ๊ณ Spring Bean ์์ ์์ด์ผ ํฉ๋๋ค.)
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class MySlowService {
@Async // ์ด๋ ๊ฒ ํ์๋ฉด ๋ฉ๋๋ค.
public void doSomethingSlow() {
System.out.println("๋๋ฆฐ ์์
์์... (๋ค๋ฅธ ์ค๋ ๋์์)");
// ... ์๊ฐ ๊ฑธ๋ฆฌ๋ ์์
...
System.out.println("๋๋ฆฐ ์์
๋!");
}
}
๋จ์ํ '์คํํ๊ณ ์์ด๋ฒ๋ฆฌ๋(Fire-and-Forget)' ์์ ์ด๋ผ๋ฉด ์์ฒ๋ผ void๋ก ์ถฉ๋ถํ์ง๋ง, ์ ๊ฒฝ์ฐ์๋ ์ค๋งํธ ์ปจํธ๋ํธ ๋ฑ๋ก์ด ์ฑ๊ณตํ๋์ง ์คํจํ๋์ง ์์์ผ ํ์ต๋๋ค.
์ด๋ด ๋ ์ ์ฉํ๊ฒ ์ฌ์ฉํ ์ ์๋ ๊ฒ์ด ๋ฐ๋ก CompletableFuture!
๋น๋๊ธฐ ์์ ์ ๊ฒฐ๊ณผ๋ฅผ ๋์ค์ ๋ฐ์๋ณด๊ฑฐ๋, ์ฑ๊ณต/์คํจ์ ๋ฐ๋ฅธ ํ์ ์ฒ๋ฆฌ๋ฅผ ๊น๋ํ๊ฒ ํ ์ ์๊ฒ ํด์ค๋๋ค.
์ ํฌ๊ฐ ์ค๋งํธ ์ปจํธ๋ํธ์ ํธ๋์ญ์ ์ ๋ฑ๋กํ๋ addContract๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ๋ณ๊ฒฝํ์์ต๋๋ค. ๐
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
@Component
public class ContractHandler {
// ... (์์ฑ์ ๋ฑ) ...
@Async // ๋น๋๊ธฐ ์คํ!
public CompletableFuture<Boolean> addContract(ContractInput input) { // ๋ฐํ ํ์
๋ณ๊ฒฝ!
try {
// ... (๋ธ๋ก์ฒด์ธ๊ณผ ํต์ ํ๋ ๋ก์ง) ...
boolean isSuccess = blockchainTransaction.isStatusOK(); // ์ฑ๊ณต ์ฌ๋ถ ํ์ธ
log.info("โ
์ค๋งํธ ์ปจํธ๋ํธ ๋ฑ๋ก ์ฑ๊ณต!");
return CompletableFuture.completedFuture(isSuccess); // ๊ฒฐ๊ณผ๋ฅผ ๋ด์ ์๋ฃ๋ Future ๋ฐํ
} catch (Exception e) {
log.error("โ ์ค๋งํธ ์ปจํธ๋ํธ ๋ฑ๋ก ์คํจ!: {}", e);
return CompletableFuture.completedFuture(false); // ์คํจ ์ false ๋ฐํ
}
}
}
ํธ์ถํ๋ ์ชฝ์์๋ ์ด๋ ๊ฒ ๊ฒฐ๊ณผ๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์์ต๋๋ค. ๐
CompletableFuture<Boolean> future = contractHandler.addContract(input);
future.thenAccept(success -> { // ์ฑ๊ณตํ์ ๋ (๋ค๋ฅธ ์ค๋ ๋์์ ์คํ๋จ)
if (success) {
log.info("โจ ์ค๋งํธ ์ปจํธ๋ํธ ๋ฑ๋ก ์ฑ๊ณต ์๋ฆผ ๋ณด๋ด์ผ์ง!");
} else {
log.error("๐ง ์คํจ ์๋ฆผ ๋ณด๋ด์ผ๊ฒ ๋ค...");
}
}).exceptionally(ex -> { // ์์ธ ๋ฐ์ํ์ ๋ (๋ค๋ฅธ ์ค๋ ๋์์ ์คํ๋จ)
log.error("๐ฅ ์์ธ ๋ฐ์!", ex);
return null;
});
log.info("๋ฉ์ธ ์ค๋ ๋๋ ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ ๋ค์ ์ผ์ ํ๋ฌ ๊ฐ๋๋ค.");
์, ์ด์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ ์๊ฐ์ ๋๋ค! @Async ๋ฉ์๋๋ฅผ Mockito๋ก Mockingํ๋๋ฐ... ์ฌ๊ธฐ์ ์ ๊ฐ ์ ์ ์ด๋ ค์์ ๊ฒช์๋ ๋ถ๋ถ์ด ์์์ต๋๋ค. ๐
// given
when(mockContractHandler.addContract(any()))
.thenReturn(new CompletableFuture<>()); // ํ
๋น Future ๋ฐํ...
// when
CompletableFuture<Boolean> future = service.callAddContract();
// then
assertTrue(future.join()); // ์ฌ๊ธฐ์ ์์ํ ๋๊ธฐ... ๐ต
new CompletableFuture<>()๋ ์๋ฃ๋์ง ์์ Future๋ฅผ ๋ง๋ค์ด์ join()์ด ๊ณ์ ๊ธฐ๋ค๋ฆฌ๊ฒ ๋ง๋ญ๋๋ค.
ํ ์คํธ ์๋๋ฆฌ์ค์ ๋ง๊ฒ ์ด๋ฏธ ์๋ฃ๋ Future๋ฅผ ๋ฐํํด์ฃผ์ด์ผ ํฉ๋๋ค.
when(mockContractHandler.addContract(any()))
.thenReturn(CompletableFuture.completedFuture(true)); // ์ฑ๊ณต(true)์ผ๋ก ์๋ฃ๋ Future!
when(mockContractHandler.addContract(any()))
.thenReturn(CompletableFuture.completedFuture(false)); // ์คํจ(false)๋ก ์๋ฃ๋ Future!
package com.ssafy.chaing.blockchain.handler;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.ssafy.chaing.blockchain.config.Web3jConnectionManager;
import com.ssafy.chaing.blockchain.handler.contract.ContractHandler;
import com.ssafy.chaing.blockchain.handler.contract.input.ContractInput;
import com.ssafy.chaing.blockchain.handler.contract.input.LiveAccountInput;
import com.ssafy.chaing.blockchain.handler.contract.input.PaymentInfoInput;
import com.ssafy.chaing.blockchain.handler.contract.output.ContractOutput;
import com.ssafy.chaing.blockchain.handler.contract.output.ContractOverviewOutput;
import com.ssafy.chaing.blockchain.handler.contract.output.ContractRentOutput;
import com.ssafy.chaing.blockchain.web3j.ContractManager;
import com.ssafy.chaing.blockchain.web3j.ContractManager.PaymentInfo;
import java.math.BigInteger;
import java.util.List;
import java.util.concurrent.CompletableFuture;
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.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.tuples.generated.Tuple13;
import org.web3j.tuples.generated.Tuple3;
import org.web3j.tuples.generated.Tuple6;
// TransactionManager๋ ์ด์ ์ง์ ์ฃผ์
๋ฐ์ง ์์
@ExtendWith(MockitoExtension.class) // JUnit5 ์ Mockito ์ฐ๋
class ContractHandlerTest {
private final String TEST_CONTRACT_ADDRESS = "0x๋ก ์์ํ๋ ์ค๋งํธ ์ปจํธ๋ํธ ์ฃผ์๊ฐ ๋ค์ด๊ฐ๋ฉด ๋ฉ๋๋ค.";
private final long TEST_CHAIN_ID = 137L; // ํ
์คํธ์ฉ ์ฒด์ธ ID
@Mock
private Web3jConnectionManager mockConnectionManager; // ์์ : ConnectionManager Mock
@Mock
private Credentials mockCredentials; // ์์ : Credentials Mock
@Mock
private ContractManager mockContractManager; // ์์ : ContractManager๋ ์ฌ์ ํ Mock ํ์
@Mock
private Web3j mockWeb3j; // ์์ : execute ์ฝ๋ฐฑ์ ์ ๋ฌ๋ Web3j Mock
// @InjectMocks ์ฌ์ฉ ์ Mockito๊ฐ ์์ฑ์์ Mock ๊ฐ์ฒด๋ค์ ์ฃผ์
์๋
// ๋จ, ์์ฑ์ ์ฃผ์
์ธ @Value ๋ฑ์ด ์์ผ๋ฉด ์ง์ ์์ฑํด์ผ ํ ์ ์์
private ContractHandler contractHandler;
// --- Helper for mocking connectionManager.execute ---
// ์ด Answer๋ connectionManager.execute๊ฐ ํธ์ถ๋ ๋
// 1. ContractManager.load๊ฐ mockContractManager๋ฅผ ๋ฐํํ๋๋ก ์ค์ ํ๊ณ (์ ์ ๋ฉ์๋ ๋ชจํน ํ์)
// 2. ์ ๋ฌ๋ ๋๋ค(callable)๋ฅผ ์คํํ๋ ๊ฒ์ ์๋ฎฌ๋ ์ด์
ํฉ๋๋ค.
// ์ ์ ๋ฉ์๋ ๋ชจํน ๋์ , ๋๋ค๊ฐ ๋ฐํํด์ผ ํ ์ต์ข
๊ฒฐ๊ณผ๋ง ์ค์ ํ๋ ๊ฒ์ด ๋ ๊ฐ๋จํฉ๋๋ค.
private <T> Answer<T> simulateExecution(T expectedResult) {
return invocation -> {
// 1. invocation์์ ๋๋ค(Web3jCallable) ๊ฐ์ ธ์ค๊ธฐ (์ ํ์ )
// Web3jConnectionManager.Web3jCallable<T> callable = invocation.getArgument(0);
// 2. ๋๋ค๊ฐ ์คํ๋ ๋ ๋ฐํ๋ ๊ฒฐ๊ณผ (๊ฐ์ฅ ์ค์)
// ์ค์ ๋๋ค ์คํ ๋์ , ๋๋ค ์คํ์ *๊ฒฐ๊ณผ*๋ฅผ ๋ฐํํ๋๋ก ์ค์ ํฉ๋๋ค.
// ์ด ๊ฒฐ๊ณผ๋ ๋ณดํต ContractManager์ ๋ฉ์๋ ํธ์ถ(.send()) ๊ฒฐ๊ณผ์
๋๋ค.
// ๋ฐ๋ผ์ ๊ฐ ํ
์คํธ ๋ฉ์๋์์ mockContractManager์ ๋์์ ๋ฏธ๋ฆฌ ์ค์ ํด๋์ด์ผ ํฉ๋๋ค.
return expectedResult;
};
}
// execute๊ฐ ์์ธ๋ฅผ ๋์ง๋๋ก ์๋ฎฌ๋ ์ด์
ํ๋ Answer
private <T> Answer<T> simulateExecutionWithError(Exception exceptionToThrow) {
return invocation -> {
throw exceptionToThrow;
};
}
@BeforeEach
void setUp() {
// MockitoAnnotations.openMocks(this) ๋์ @ExtendWith(MockitoExtension.class) ์ฌ์ฉ
// @InjectMocks๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ์๋์ผ๋ก ์์ฑ์ ํธ์ถ
contractHandler = new ContractHandler(
mockConnectionManager,
mockCredentials,
TEST_CHAIN_ID,
TEST_CONTRACT_ADDRESS
);
// CustomGasProvider๋ ๋ด๋ถ์ ์ผ๋ก new๋ก ์์ฑ๋๋ฏ๋ก ๋ณ๋ ์ฃผ์
๋ถํ์
// ContractManager๋ loadContractManager ํฌํผ ๋ด์์ ๋ก๋๋๋ฏ๋ก ํ๋ ์ฃผ์
๋ถํ์
}
@Test
void testAddContract_Success() throws Exception { // CompletableFuture ์์ธ ์ฒ๋ฆฌ๋ฅผ ์ํด throws Exception ์ถ๊ฐ
// --- Input Data ---
PaymentInfoInput paymentInfo1 = new PaymentInfoInput(BigInteger.valueOf(1), BigInteger.valueOf(2100000),
BigInteger.valueOf(7));
// ... (๋ค๋ฅธ PaymentInfoInput)
ContractInput input = new ContractInput(/* ... input data ์ค์ ... */);
input.setId(BigInteger.ONE);
input.setPaymentInfos(List.of(paymentInfo1)); // ์์
// --- Mocking ---
// 1. ์ต์ข
๊ฒฐ๊ณผ์ธ TransactionReceipt Mock ์ค์
TransactionReceipt mockReceipt = mock(TransactionReceipt.class);
when(mockReceipt.isStatusOK()).thenReturn(true);
// 2. connectionManager.execute๊ฐ ํธ์ถ๋๋ฉด mockReceipt๋ฅผ ๋ฐํํ๋๋ก ์ค์
// any()๋ฅผ ์ฌ์ฉํ์ฌ ์ด๋ค Web3jCallable์ด๋ ๋์ผํ๊ฒ ๋์ํ๋๋ก ์ค์
when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
.thenAnswer(simulateExecution(mockReceipt)); // ์ฑ๊ณต ์ Receipt ๋ฐํ
// --- Execution ---
CompletableFuture<Boolean> futureResult = contractHandler.addContract(input);
// --- Verification ---
assertTrue(futureResult.join(), "Contract should be added successfully");
// connectionManager.execute๊ฐ ์ ํํ 1๋ฒ ํธ์ถ๋์๋์ง ๊ฒ์ฆ (์ ํ์ )
verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
}
@Test
void testAddContract_Failure_ExceptionDuringExecution() throws Exception {
// --- Input Data ---
ContractInput input = new ContractInput(/* ... input data ์ค์ ... */);
input.setId(BigInteger.TWO);
// --- Mocking ---
// connectionManager.execute๊ฐ ํธ์ถ๋ ๋ RuntimeException์ ๋์ง๋๋ก ์ค์
RuntimeException simulatedException = new RuntimeException("Blockchain connection failed");
when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
.thenAnswer(simulateExecutionWithError(simulatedException));
// --- Execution ---
CompletableFuture<Boolean> futureResult = contractHandler.addContract(input);
// --- Verification ---
assertFalse(futureResult.join(), "Should return false when an exception occurs");
// connectionManager.execute๊ฐ ํธ์ถ๋์๋์ง ๊ฒ์ฆ
verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
}
@Test
void testGetContract() throws Exception {
// --- Mocking Data ---
BigInteger contractId = BigInteger.ONE;
Tuple13<BigInteger, String, String, BigInteger, BigInteger, String, String, BigInteger, List<PaymentInfo>, String, Boolean, BigInteger, BigInteger> dummyTuple =
new Tuple13<>(
contractId, "2025-01-01Z", "2025-12-31Z", BigInteger.valueOf(3000000), BigInteger.valueOf(5),
"112233445566", "998877665544", BigInteger.TEN,
List.of(new PaymentInfo(new Uint256(1), new Uint256(2100000), new Uint256(7))),
"123456789012", true, BigInteger.valueOf(3), BigInteger.valueOf(123)
);
// --- Mocking ---
// connectionManager.execute๊ฐ ํธ์ถ๋๋ฉด ์ต์ข
๊ฒฐ๊ณผ์ธ dummyTuple์ ๋ฐํํ๋๋ก ์ค์
when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
.thenAnswer(simulateExecution(dummyTuple));
// --- Execution ---
ContractOutput result = contractHandler.getContract(contractId);
// --- Verification ---
assertNotNull(result);
assertEquals(contractId, result.getId());
assertEquals("2025-01-01Z", result.getStartDate());
// ... (๋ค๋ฅธ ํ๋ ๊ฒ์ฆ)
// connectionManager.execute๊ฐ 1๋ฒ ํธ์ถ๋์๋์ง ๊ฒ์ฆ
verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
// ์ค์: contractManager ์์ฒด์ ๋ฉ์๋ ํธ์ถ์ ์ง์ ๊ฒ์ฆํ๋ ๋์ ,
// connectionManager.execute์ ํธ์ถ๊ณผ ๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ์ฆํฉ๋๋ค.
}
@Test
void testGetContractOverview() throws Exception {
// --- Mocking Data ---
BigInteger contractId = BigInteger.ONE;
Tuple3<BigInteger, String, String> dummyTuple =
new Tuple3<>(contractId, "2025-01-01", "2025-12-31");
// --- Mocking ---
when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
.thenAnswer(simulateExecution(dummyTuple));
// --- Execution ---
ContractOverviewOutput result = contractHandler.getContractOverview(contractId);
// --- Verification ---
assertNotNull(result);
assertEquals(contractId, result.getId());
assertEquals("2025-01-01", result.getStartDate());
verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
}
// ... (getPaymentInfoCount, getRentData ๋ฑ ๋ค๋ฅธ ์ฝ๊ธฐ ํ
์คํธ๋ ์ ์ฌํ๊ฒ ์์ ) ...
// ์์: getRentData
@Test
void testGetRentData() throws Exception {
// --- Mocking Data ---
BigInteger contractId = BigInteger.ONE;
Tuple6<BigInteger, BigInteger, String, String, BigInteger, BigInteger> dummyTuple =
new Tuple6<>(
BigInteger.valueOf(3000000), BigInteger.valueOf(5), "112233445566",
"665544332211", BigInteger.TEN, BigInteger.valueOf(3)
);
// --- Mocking ---
when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
.thenAnswer(simulateExecution(dummyTuple));
// --- Execution ---
ContractRentOutput result = contractHandler.getRentData(contractId);
// --- Verification ---
assertNotNull(result);
assertEquals("112233445566", result.getRentAccountNo());
verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
}
@Test
void testAddLiveAccount_Success() throws Exception {
// --- Input ---
BigInteger contractId = BigInteger.ONE;
LiveAccountInput liveAccountInput = new LiveAccountInput("validAccountNo");
// --- Mocking ---
TransactionReceipt mockReceipt = mock(TransactionReceipt.class);
when(mockReceipt.isStatusOK()).thenReturn(true);
when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
.thenAnswer(simulateExecution(mockReceipt));
// --- Execution ---
boolean result = contractHandler.addLiveAccount(contractId, liveAccountInput);
// --- Verification ---
assertTrue(result, "Adding live account should succeed");
verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
}
@Test
void testAddLiveAccount_Failure_ReceiptNotOk() throws Exception {
// --- Input ---
BigInteger contractId = BigInteger.ONE;
LiveAccountInput liveAccountInput = new LiveAccountInput("validAccountNo");
// --- Mocking ---
TransactionReceipt mockReceipt = mock(TransactionReceipt.class);
when(mockReceipt.isStatusOK()).thenReturn(false); // ํธ๋์ญ์
์คํจ ์๋ฎฌ๋ ์ด์
when(mockConnectionManager.execute(any(Web3jConnectionManager.Web3jCallable.class)))
.thenAnswer(simulateExecution(mockReceipt));
// --- Execution ---
boolean result = contractHandler.addLiveAccount(contractId, liveAccountInput);
// --- Verification ---
assertFalse(result, "Should return false when transaction receipt is not OK");
verify(mockConnectionManager, times(1)).execute(any(Web3jConnectionManager.Web3jCallable.class));
}
}