일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Logback
- docker-compose
- 스프링 시큐리티
- ZuulFilter
- 트리
- 구간 트리
- 다익스트라
- 완전 탐색
- 백트래킹
- 이분 탐색
- Gradle
- 유레카
- 비트마스킹
- 메모이제이션
- spring boot
- 서비스 디스커버리
- spring cloud
- 플로이드 와샬
- Zuul
- 이분 매칭
- 달팽이
- dp
- 도커
- 주울
- Java
- BFS
- 구현
- Spring Cloud Config
- 스택
- 게이트웨이
- Today
- Total
Hello, Freakin world!
[채팅앱][클라이언트] ClientChannelManager 단위 테스트 작성하기 본문
다시 ClientChannelManager 클래스로 돌아와서 작성했던 기능들의 단위테스트를 작성해보자.
아래는 ClientChannelManager 클래스의 코드다.
import ...
/*
이 클래스의 책임은 어디까지인가?
단순하게 채널 객체의 생성과 반환만을 책임진다.
*/
public class ClientChannelManager {
private SocketChannel socketChannel;
private Logger logger = Logger.getLogger(ClientChannelManager.class.getCanonicalName());
private SocketAddress serverAddress;
public ClientChannelManager(){}
public synchronized SocketChannel getChannelInstance(){
if(socketChannel == null || !socketChannel.isConnected() ||
!socketChannel.isOpen()) {
open();
boolean isConnected = connect(serverAddress);
if (isConnected) {
return socketChannel;
}
}
return socketChannel;
}
private void open() {
try {
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
logger.info("[클라이언트] 채널 open 실패 : " + e.getMessage());
}
}
private boolean connect(SocketAddress address){
try {
socketChannel.connect(address);
return socketChannel.finishConnect();
} catch (IOException e) {
e.printStackTrace();
logger.info("[클라이언트] 서버와 connect 실패 : " + e.getMessage());
return false;
} catch (ConnectionPendingException e) {
e.printStackTrace();
logger.info("[클라이언트] 잘못된 서버 address 입니다.");
return false;
}
}
public void setServerAddress(SocketAddress serverAddress) {
this.serverAddress = serverAddress;
}
}
모든 메서드에 대해서 단위 테스트를 할 필요는 없다.
위 클래스 중 private 접근자를 가진 메서드는 단순히 자바 api를 감싼 메서드일 뿐이다.
굳이 신뢰성이 보장된 라이브러리의 테스트까지 하는 건 시간낭비가 아닐까?
(하지만 private 메서드라도 종종 테스트가 필요한 메서드가 생길 수도 있는데. 그럴 땐 자바의 reflection api를 사용해 테스트를 작성할 수 있다.)
결국 테스트가 필요한 메서드는 getChannelInstance() 메서드이다.
이 메서드를 테스트하기 위해서는 Mock 서버가 필요하다.
아래는 간단하게 클라이언트의 연결 요청만 받아주는 MockServer 클래스다.
import ...
public class MockServer extends Thread{
public Selector selector;
public ServerSocketChannel serverSocketChannel;
SocketAddress address;
public boolean isContinue = true;
public MockServer(SocketAddress address) {
this.address = address;
}
public void init() {
try {
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(address);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(isContinue) {
try {
selector.select();
} catch (IOException e) {
e.printStackTrace();
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
selectionKeys.stream()
.filter(SelectionKey::isAcceptable)
.forEach(this::acceptHandler);
}
}
private void acceptHandler(SelectionKey key) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
try {
serverSocketChannel.accept();
} catch (IOException e) {
e.printStackTrace();
}
}
}
이 클래스의 중요한 포인트는 Junit 테스트 중 비동기로 실행되어 각 테스트 메서드들의 연결 요청을 처리할 수 있어야 한다는 것이다.
처음에는 내부에 main 메서드를 만들어 테스트 시작 전에 실행시켜 두었는데, 진짜 너무 구린 방식이다.
이런 방식으로는 자동화된 테스트들을 만들 수 없다.
그래서 Junit 애너테이션 정보들을 찾아보니 @BeforeClass 라는 쓸만한 녀석을 발견했다.
이 애너테이션을 public static void 메서드 위에 달아놓으면 그 메서드는 모든 테스트 메서드 실행 전, 딱 한번 실행된다. (@AfterClass도 동일한 개념이다.)
이를 바탕으로 작성한 테스트 메서드다.
import ...
public class TestClientChannelManager {
ClientChannelManager manager;
SocketAddress fakeAddress;
SocketAddress correctAddress;
static MockServer mockServer;
@BeforeClass
public static void beforeClass() {
System.out.println("Mock 서버 시작");
mockServer = new MockServer(new InetSocketAddress("localhost", 9090));
mockServer.init();
mockServer.start();
}
@Before
public void before() {
manager = new ClientChannelManager();
fakeAddress = new InetSocketAddress("localhost", 9005);
correctAddress = new InetSocketAddress("localhost", 9090);
}
@AfterClass
public static void afterClass() throws IOException {
mockServer.serverSocketChannel.close();
mockServer.isContinue = false;
mockServer.selector.wakeup();
}
/*
연결에 성공해서 연결된 채널 객체를 반환함.
반환된 객체가 null 이 아닌지 확인.
*/
@Test
public void testGetConnetedChannel_whenSuccess() {
manager.setServerAddress(correctAddress);
assertThat(manager.getChannelInstance().isConnected()).isTrue();
}
/*
처음 getConnectedChannelInstance 을 호출해서 반환에 실패했을 때
null 을 반환하는가?
*/
@Test
public void testGetConnetedChannel_whenFirstInitiatingIsFailed(){
manager.setServerAddress(fakeAddress); //임의의 address 객체
assertThat(manager.getChannelInstance().isConnected()).isFalse();
}
/*
연결된 채널을 한번 close 한 후 다시
잘못된 주소로 생성하는 시도에서 실패할 경우 연결되지 않은 채널 객체를 반환하는가?
어떻게 테스트할 것인가?
*/
@Test
public void testGetConnetedChannel_whenSecondlyInitiatingIsFailed() throws IOException {
manager.setServerAddress(correctAddress);
SocketChannel channel = manager.getChannelInstance();
channel.close();
manager.setServerAddress(fakeAddress);
channel = manager.getChannelInstance();
assertThat(channel.isConnected()).isFalse();
}
/*
같은 포트에 close 했다가 다시 연결하는 경우.
*/
@Test
public void testGetConnetedChannel_whenSecondlyInitiatingIsSuccessful() throws IOException {
manager.setServerAddress(correctAddress);
SocketChannel channel = manager.getChannelInstance();
channel.close();
channel = manager.getChannelInstance();
assertThat(channel.isConnected()).isTrue();
}
}
'Toy Project > 채팅 앱 만들기' 카테고리의 다른 글
[채팅앱][클라이언트] 이벤트 아키텍쳐 설계 (0) | 2020.02.27 |
---|---|
[채팅앱][전체] 앱의 전체적인 흐름 ver 1.0 (0) | 2020.02.27 |
[채팅앱][클라이언트] IO 단위 테스트 어떻게 할 것인가? (0) | 2020.02.21 |
[채팅앱][서버] EventLoop 스레드 설계 (0) | 2020.02.21 |
[채팅앱][서버] non-blocking IO 다중 연결 서버 아키텍처 (0) | 2020.02.21 |