ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 테스트를 짤 때 느껴지는 이상함은 구조 분리의 신호
    Today I Learned 2022. 11. 14. 06:33

     

    어제 사용자가 신청하지 않은 운동 게시글에 운동을 신청했을 때, 예외가 발생할 경우 예외 메세지를 출력하게 해주는 로직의 UI 컴포넌트 부분과 테스트 코드를 작성하는 과정에서 겪었던 이슈가 있었다.

     

    아래의 소스코드와 테스트 코드를 보자.

     

    // src/components/Posts.jsx 
    
    import styled from 'styled-components';
    
    // Definitions of styled components
    // ...
    
    export default function Posts({
      posts,
      postsErrorMessage,
      registerErrorCodeAndMessage,
      registerToGame,
      cancelRegisterGame,
    }) {
      const handleRegisterToGameClick = (gameId) => {
        registerToGame(gameId);
      };
      const handleCancelRegisterGameClick = (gameId) => {
        cancelRegisterGame(gameId);
      };
    
      if (postsErrorMessage) {
        return (
          <p>{postsErrorMessage}</p>
        );
      }
    
      if (posts.length === 0) {
        return (
          <p>등록된 게시물이 존재하지 않습니다.</p>
        );
      }
      
      return (
        <Container>
          <Thumbnails>
            {posts.map((post) => (
              <Thumbnail key={post.id}>
                <div>
                  // Contents of post
                  // ...
                </div>
                <div>
                  {post.game.isRegistered ? (
                    <button
                      type="button"
                      onClick={() => handleCancelRegisterGameClick(post.game.id)}
                    >
                      신청취소
                    </button>
                  ) : (
                    <button
                      type="button"
                      onClick={() => handleRegisterToGameClick(post.game.id)}
                    >
                      신청
                    </button>
                  )}
                  {registerErrorCodeAndMessage.message ? (
                    <p>
                      {registerErrorCodeAndMessage.message}
                    </p>
                  ) : (
                    null
                  )}
                </div>
              </Thumbnail>
            ))}
          </Thumbnails>
        </Container>
      );
    }
    // src/components/Posts.test.jsx
    
    describe('Posts', () => {
      const registerToGame = jest.fn();
      const cancelRegisterGame = jest.fn();
    
      const renderPosts = ({
        posts,
        postsErrorMessage,
      }) => {
        render((
          <Posts
            posts={posts}
            postsErrorMessage={postsErrorMessage}
            registerErrorCodeAndMessage={registerErrorCodeAndMessage}
            registerToGame={registerToGame}
            cancelRegisterGame={cancelRegisterGame}
          />
        ));
      };
      
      context('등록된 게시글이 존재하는 경우', () => {
        const posts = [
          {
            // Contents of post 1
          },
          {
            // Contents of post 2
          },
        ];
        const postsErrorMessage = '';
        // Other values
        // ...
    
        it('각 게시물의 썸네일 출력', () => {
          renderPosts({ 
          	posts,
            postsErrorMessage,
            registerErrorCodeAndMessage,
            registerToGame,
            cancelRegisterGame,
          });
    
          // Expect methods
        });
        
        // 신청/취소 버튼 클릭 시 이벤트 핸들러 호출 테스트 context
        // ...
      });
      
      // 등록된 게시글이 존재하지 않는 경우 context
      // ...
    
      context('게임이 찾아지지 않은 에러가 발생한 경우', () => {
        const posts = [];
        const postsErrorMessage = '주어진 게임 번호에 해당하는 게임을 찾을 수 없습니다.';
        // Other values
        // ...
    
        it('에러 메세지 출력', () => {
          renderPosts({ 
          	posts,
            postsErrorMessage,
            registerErrorCodeAndMessage,
            registerToGame,
            cancelRegisterGame,
          });
    
          screen.getByText(/주어진 게임 번호에 해당하는 게임을 찾을 수 없습니다./);
        });
      });
    });

     

    테스트 코드를 짜면서 이상한 점이 느껴졌다. 예를 들면 에러 종류 중 하나인 게임이 찾아지지 않은 에러가 발생한 경우의 에러 메세지가 UI에 출력되는지만 검증 부분을 집중하고 싶었다. 그렇지만 테스트를 수행하기 위해서는 게시물과 관련된 데이터를 같이 넣어주어야 했다.

     

    빈 배열을 넣어주는 식으로 넘어갈 수도 있었겠지만, 이 시점에서는 하나의 게시물에서 컴포넌트의 관심사가 나뉘어야 할 것 같다는 생각이 들었다. 컴포넌트를 한번 분리시켜보기로 했다.

     

    게시글과 경기 정보를 출력하는 UI 컴포넌트와, 신청/신청취소 버튼과 예외 메세지가 출력되는 UI 컴포넌트의 분리를 시도했다. Posts.jsx에서 생성하는 하위 컴포넌트들 중 하나인 PostsRegisterButton.jsx와 그 테스트 코드는 다음과 같다.

     

    // src/components/Posts.jsx의 하위 컴포넌트 생성 
    
    <Thumbnail key={post.id}>
        <PostsContent
          hits={post.hits}
          type={post.game.type}
          date={post.game.date}
          place={post.game.place}
          currentMemberCount={post.game.currentMemberCount}
          targetMemberCount={post.game.targetMemberCount}
        />
        <PostsRegisterButton
          gameId={post.game.id}
          isRegistered={post.game.isRegistered}
          registerToGame={registerToGame}
          cancelRegisterGame={cancelRegisterGame}
          registerErrorCodeAndMessage={registerErrorCodeAndMessage}
        />
    </Thumbnail>
    // src/components/PostsRegisterButton.jsx
    
    export default function PostsRegisterButton({
      gameId,
      isRegistered,
      registerToGame,
      cancelRegisterGame,
      registerErrorCodeAndMessage,
    }) {
      const handleRegisterToGameClick = (id) => {
        registerToGame(id);
      };
    
      const handleCancelRegisterGameClick = (id) => {
        cancelRegisterGame(id);
      };
    
      return (
        <Container>
          {isRegistered ? (
            <button
              type="button"
              onClick={() => handleCancelRegisterGameClick(gameId)}
            >
              신청취소
            </button>
          ) : (
            <button
              type="button"
              onClick={() => handleRegisterToGameClick(gameId)}
            >
              신청
            </button>
          )}
          {registerErrorCodeAndMessage.message ? (
            <p>
              {registerErrorCodeAndMessage.message}
            </p>
          ) : (
            null
          )}
        </Container>
      );
    }
    // src/components/PostsRegisterButton.test.jsx
    
    describe('PostsRegisterButton', () => {
      const registerToGame = jest.fn();
      const cancelRegisterGame = jest.fn();
    
      const renderPostsRegisterButton = ({
        gameId,
        isRegistered,
        registerErrorCodeAndMessage,
      }) => {
        render((
          <PostsRegisterButton
            gameId={gameId}
            isRegistered={isRegistered}
            registerToGame={registerToGame}
            cancelRegisterGame={cancelRegisterGame}
            registerErrorCodeAndMessage={registerErrorCodeAndMessage}
          />
        ));
      };
    
      context('신청 버튼이 있으면 신청 버튼 클릭 시', () => {
        const gameId = 2;
        const isRegistered = false;
        const registerErrorCodeAndMessage = {};
    
        it('운동 참가 신청 이벤트 핸들러 호출', () => {
          jest.clearAllMocks();
          renderPostsRegisterButton({
            gameId,
            isRegistered,
            registerErrorCodeAndMessage,
          });
    
          fireEvent.click(screen.getByText('신청'));
          const expectedGameId = 2;
          expect(registerToGame).toBeCalledWith(expectedGameId);
        });
      });
    
      context('신청 취소 버튼이 있으면 신청 취소 버튼 클릭 시', () => {
        const gameId = 1;
        const isRegistered = true;
        const registerErrorCodeAndMessage = {};
    
        it('운동 참가 취소 이벤트 핸들러 호출', () => {
          jest.clearAllMocks();
          renderPostsRegisterButton({
            gameId,
            isRegistered,
            registerErrorCodeAndMessage,
          });
    
          fireEvent.click(screen.getByText('신청취소'));
          const expectedGameId = 1;
          expect(cancelRegisterGame).toBeCalledWith(expectedGameId);
        });
      });
    
      context('신청 후 에러가 발생해 에러 상태가 전달되었을 시', () => {
        const gameId = 10;
        const isRegistered = false;
        const registerErrorCodeAndMessage = {
          code: 101,
          message: '이미 신청이 완료된 운동입니다.',
        };
    
        it('에러 메세지 출력', () => {
          renderPostsRegisterButton({
            gameId,
            isRegistered,
            registerErrorCodeAndMessage,
          });
    
          screen.getByText('이미 신청이 완료된 운동입니다.');
        });
      });
    });

     

    분리 결과, 컴포넌트의 수는 늘어났지만, 각 컴포넌트의 크기가 작아져 내용을 파악하기 위한 인지 부하가 줄어들었고, 하나의 컴포넌트에서 테스트를 수행할 때 필요한 데이터만을 이용해 검증을 수행할 수 있게 되었다.

     

    테스트 코드를 작성하면서 느껴지는 이상 신호를 그냥 넘기지 말고, 소스코드의 구조를 개선해야 한다는 신호로 받아들일 수 있도록 하자.

     

     

    댓글

Designed by Tistory.