김서버의 프론트엔드 일기
단위테스트 (독후감) 본문
단위 테스트 - 생산성과 품질을 위한 단위테스트 원칙과 패턴은 블라디미르 코리코프가 지은 책으로 단위테스트의 정의 부터 출발하여, 어떤 테스트가 좋은테스트이고 또 실무에서는 주로 어떻게 적용이 되는지 통합테스트, 단위테스트, E2E테스트의 차이에 대한 설명을 하고 있습니다.
개인적인 독후감이기 때문에 정말 인상 깊은 부분만 정리해서 적었습니다.
1. 단위 테스트의 목표
단위 테스트의 목표는 소프트웨어 프로젝트의 지속 가능한 성장을 가능하게 하는 것 입니다. 여기서 지속 가능한이라고 하는 이유는 프로젝트가 시작하는 초창기에는 크게 느끼지 못한다는 점은 당연히 있습니다. 왜냐하면 테스트의 부재로 나타나는 불편함은 코드가 거대한 덩어리가 되어 몇 만줄 단위가 되었을 때, 오로지 뇌피셜로만 디버깅하고 코드를 기능을 추가해야하는 경우에 이런 불편함을 느끼게 됩니다.
실제로 주위 개발자 어느 분의 이야기가 생각이 났습니다. 어떤 기능을 추가하거나 고치려고하면 분석하는데만 2~3일 이상이 걸리고, 개발하는데 1~2일이 걸리고 QA와 커뮤니케이션 하며 하나의 기능을 가지고 이야기를 3일 이상하는 것이 일상 이라고 합니다.
이 모든 절차를 거치게 되면 기능 작은거 하나에도 일주일 이상씩 걸린다고 합니다. 결국 그 팀은 현재 기능을 넣을 때, 대충 굴러가기만 하면 넣은 기능이 이미 몇 개가 있다고 합니다.
이렇게 코드가 거대해지는 것에 따라 개발 속도가 빠르게 감소하는 현상을 소프트웨어 엔드로피 라고도 합니다. 여기서 엔드로피는 시스템 내 무질서도라고 하며, 그 프로젝트의 품질 자체를 떨어뜨리는 한 마디로 레거시 덩어리가 되어 결국엔 차세대로 갈아 엎는 작업을 하게 만드는 원인을 제공하는 것을 말합니다.
테스트코드가 잘짜여있지 않으면 물론 이 차세대로 프로젝트를 갈아 엎는 것 조차도 몇 십명의 개발자들을 데리고 1년 넘게 하게 되는 경우도 빈번하고 또 기능이 빵꾸나거나 버그가 나와서 다시 몇 개월 고생하는 것은 지난 오랜 역사에서 느끼고 있다고 생각합니다.
결국 단위 테스트라는 것은 프로젝트에 기여한 사람과 코드의 양이 많아져버렸을 때, 차세대 프로젝트를 해야하는 상황을 방지를 하거나 차세대 프로젝트를 임하는 것에 있어서 미리 힌트를 주어 더 쉽게 할 수 있도록 하는 것이 단위 테스트를 하는 이유가 되겠습니다.
2. 단위 테스트의 정의
단위테스트를 정의하는 데에는 크게 두 가지 파벌(?)로 나뉘어서 정의를 하고 있습니다. 그것은 고전파와 런던파로 알려져 있고, 우선 고전파는 단위 테스트와 테스트 주도 개발에 원론적으로 접근하기 때문에 고전이라고 합니다. 반면 런던파는 런던의 프로그래밍 커뮤니티에서 시작이 되었습니다. 두 파벌의 차이는 목과 테스트 취약성에서 크게 나뉘어지게 됩니다.
우선 단위 테스트를 정의하는데에 있어서, 크게 세 가지 속성을 가지고 있습니다.
- 작은 코드 조각(단위)을 검증하고
- 빠르게 수행하고
- 격리된 방식으로 처리하는 자동화된 테스트
여기서 앞의 두개는 큰 논란의 여지가 없다고 합니다. 하지만 마지막의 격리 문제는 고전파와 런던파를 구분할 수 있게 해주는 아주 근본적인 차이에 속하게 됩니다.
2 - 1. 런던파의 격리 방식
런던파는 테스트 대상 시스템을 협력자에게서 격리하는 것을 말합니다. 즉 하나의 테스트는 오직 하나의 클래스만을 테스트 해야하고, 의존하고 있는 다른 클래스 모든 의존성을 전부 mocking 해서 작성 합니다. 이런 점 때문에 런던파는 목 추종자라고도 합니다. 이들이 이렇게 격리를 하는 이유는 외부 영향을 프로그래머가 최대한 통제해야 테스트 대상에만 집중해서 개발 할 수 있기 때문이라고 생각한다고 합니다.
2 - 2. 고전파의 격리 방식
고전파는 테스트 대상 코드 조각의 범위를 어디까지로 둘 것인가로 다시 이야기를 해야 둘의 관계가 극명하게 보이게 됩니다. 런던파는 오직 작은 해당 클래스 즉 지금 당장 내가 테스트하고 싶은 애만 테스트를 해야한다는 신념이 있습니다. 하지만 고전파의 경우는 단위 코드를 보는 것이 아닌 행동에 대한 단위를 보아야 한다고 생각을 합니다.
일단 단위테스트를 하는 것에 있어서 하나의 테스트에서는 하나의 클래스만 하는 것이 원칙이겠지만 고전파의 경우에는 클래스간의 통신의 흐름도 중요한 테스트 요소로 보기 때문에 최소한의 mocking을 하게 되고, 결국 내 프로젝트 코드에서 검증할 수 없는 경우(예를 들면 DB 커넥션 결과, 외부 API 호출 등)만 mocking 하게 되는 방식을 말합니다.
어느 부분이 익숙한지는 프론트엔드에 대입해서 표현해보겠습니다.
만약 custom hook을 import 하는 컴포넌트를 테스트 할 때, mocking 하는 것이 custom hook 자체를 하는 것을 선호하면 런던파에 가깝고, custom hook에서 사용하는 외부 라이브러리나, 외부 API 호출들을 일일히 찾아내어 mocking을 해서 custom hook의 동작과 테스트 하는 컴포넌트의 동작까지 함께 보는 것을 선호하면 고전파에 가깝다고 생각하면 됩니다.
개인적으로는 고전파에 가까운 편이며, 모킹하는 것 들로는 주로 외부 라이브러리(redux, react-router-dom)와 API 호출들을 mocking해서 테스트 하는 것을 선호 하는 편입니다.
3. 좋은 단위 테스트의 4대 요소
좋은 단위 테스트의 요소로 네가지의 특성을 가지고 있습니다.
- 회귀방지
- 리팩터링 내성
- 빠른 피드백
- 유지 보수성
이 네가지의 요소를 생각하며 테스트 코드와 코드베이스를 작성한다면 좋은 코드는 자연스럽게 나올 수 밖에 없게 됩니다. 하지만 이 네가지를 100만큼 만족하는 방법과 코드는 나올 수가 없습니다. 단지 완전히 포기해서는 안되고, 머리에서 어느정도는 양보를 하더래도 마지막까지 고민해서 코드를 작성하는 것이 좋습니다.
3 - 1. 회귀 방지
회귀는 쉽게 말하자면 소프트웨어의 버그입니다. 일반적으로 코드를 수정한 후 기능이 의도한대로 작동하지 않는 경우를 말합니다.
회귀라는 것은 사실 가장 우리에게 익숙한 부분입니다. 예를 들면 회사에 입사해서 코드를 분석하는데 컴포넌트 하나에 몇 천줄이 되는 경우가 있을 겁니다. 이 코드에 대한 테스트나 주석이 없으면 정말 최악이지만 만약에 있다고 해도 우리는 이 코드를 건들 엄두가 나지 않을 것입니다. 만약 기획자가 이 코드를 건들여야하는 일을 가지고 온다면 우리는 엄청난 리스크를 가지고 이 기능을 고쳐야 합니다. 잘 되면 당연한거고 잘못 되서 버그가 나면 욕을 먹는 건 우리의 숙명이죠...
회귀 방지를 하기 위해서는 위의 상황을 방지 할 수만 있으면 됩니다. 즉 코드를 최대한 나누고 좋은 변수명과 함수명으로 작성하고 또한 좋은 아키텍쳐 위에 코드를 작성하면 베스트지만 그렇지 않다면 규칙에 맞게만 잘 작성 해준다면 회귀 방지에 점수가 높아지게 됩니다.
3 - 2. 리팩터링 내성
리팩터링 내성은 한 마디로 테스트코드와 베이스코드가 있을 때, 테스트코드를 건들지 않고도 베이스코드만 변경이 가능한가를 보게 되는 것입니다.
실제로 우리는 테스트코드가 있는 베이스코드를 리팩터링 할 때, 베이스코드를 변경 후에 눈으로 보거나 어디로 봐도 문제가 전혀 없지만 자꾸 테스트가 오류가 나는 경험을 가지고 있습니다. 이럴 때 우리의 방법은 빨간줄 나는 테스트를 슥 지운다거나 억지로 mocking 데이터를 고쳐서 억지로 초록색이 나오게끔 한 경험이 있을 겁니다.
리팩터링 내성을 높이기 위해서는 위 상황의 원인을 찾아보면 됩니다. 대부분 테스트를 지우거나 mocking 데이터를 바꾸는 이유는 바로 그 컴포넌트가 가지는 역할과 책임을 무시하고 우리 맘대로 테스트를 과잉으로 넣어 생기는 문제가 됩니다.
이런 상황을 전문용어로 거짓 양성이라고 합니다. 즉 리팩터링 내성을 올리기 위해서는 이런 거짓 양성을 최소한으로 만드는 것이 리팩터링 내성의 점수를 올리는 가장 좋은 방법이라고 할 수 있습니다.
3 - 3. 빠른 피드백과 유지보수성
빠른 피드백은 테스트코드가 얼마나 빠르게 실행할 수 있는가를 보면 됩니다. 테스트 코드가 느리게 실행 되면 그만큼 프로그래머는 검증 할 수 있는 횟수가 줄어들게 되어 개발 속도가 상당히 느려지게 됩니다.
마지막으로 유지보수성은 테스트가 이해하기 쉬운 척도와 실행하기 쉬운 환경이 됩니다. 테스트 코드의 라인 수를 최대한을 줄이고, 또 불필요한 의존성을 지우는 것이 가장 핵심입니다. 예를 들면 한 컴포넌트의 테스트 코드가 몇 천 줄이면 이것을 볼 엄두가 날까? 또 테스트코드를 실행하는데 외부 서버 상태확인하고 DB 커넥션을 확인이 되야만 테스트를 돌릴 수 있다면 유지보수성이 꽤 떨어질 수 있습니다.
3 - 4. 유닛테스트, 통합테스트, E2E테스트
일반적으로 우리는 유닛테스트, 통합테스트, E2E테스트로 테스트 코드를 작성합니다. 좋은 테스트를 위한 4가지의 속성 중 각각의 테스트 방식은 어떤 속성들을 가지고 있는지 알아 볼 수 있습니다.
우선 세가지 모두 기본으로 유지보수성과 리팩터링 내성을 가지고 작성을 해야합니다. 이는 개발자가 조금 더 신경을 써준다면 충분히 케어할 수 있는 영역입니다.
첫 번째로 유닛테스트는 작은 함수나 클래스를 테스트 하기 때문에, 하나하나의 피드백을 빠르게 받아볼 수 있습니다. 하지만 작은 단위로 자기 것만 테스트를 하기 때문에 전체적으로 어플리케이션이 잘 돌아가는지에 대한 것은 정확히 검증할 수가 없다는 부분이 있습니다. 이해하기 어렵다면 사공(테스트 단위 함수, 클래스)들은 잘하고 든든하지만, 전체적으로 봐야하는 사람이 없어서 배가 산으로 간다고 생각하시면 편합니다.
두 번째로 E2E 테스트는 코드가 어떻게 가는지는 관심이 없고, 유저가 눌러봤을 때 문제가 없는지를 미리 테스트 코드로 작성해서 확인하기 때문에 실제로 버그가 발생해서 다시 작성해야하는 부분은 없을 수 있습니다. 하지만 내부 코드들이 잘 작동해서 화면이 나오는 것인지 알 수 없고, 개발 하는 단계에서 기능을 넣으면서 개발하기에는 많은 테스트 단계들을 거쳐야만 내가 지금 작성하는 부분을 테스트하며 개발할 수 있기 때문에 빠른 피드백에서는 굉장히 취약합니다.
세 번째로 통합테스트는 작은 단위들을 합쳐서 클래스나 함수끼리의 통신에 대한 테스트를 하게 됩니다. 이런 방법은 실제로 E2E로 테스트 하기 전에 먼저 확인을 해볼 수 있고, 단위간의 통신을 보기 때문에 중간에 전체적으로 봐주는 사람도 있어서 배가 산으로 가는 경우도 극히 드물게 됩니다. 어떻게 보면 두 가지의 장점을 고루 섞인 테스트 방식이기도 하지만 두 가지 방식의 약점을 조금씩은 가지고 있습니다.
정리하면 유닛 테스트는 빠른 피드백의 강점이 있으나 회귀 방지에는 도움이 되지 않은 경우가 많고, E2E테스트는 회귀 방지에는 도움이 되나 피드백이 엄청 느립니다. 마지막으로 통합테스트는 빠른 피드백과 회귀방지에서 어느정도의 장점을 가지고 있어서 어떻게 보면 가장 이상적인 테스트 방식입니다.
그렇다고 너무 통합테스트만 하기 보다는 필요에 따라서 팀과 협의 보며 유닛테스트, 통합테스트, E2E테스트를 고루고루 사용하는 것이 좋습니다. 프론트엔드에서는 사실 E2E와 통합테스트는 cypress 하나로 커버가 가능하고 유닛테스트는 jest를 통해서 할 수 있으니 지레 겁먹지 말고 꼭 도전해보시기 바랍니다.
4. 목과 스텁
책을 읽으면서 가장 신선하게 충격을 받은 부분이기도 합니다. 사실 mock과 stub을 위에까지만 해도 구분없이 사용해왔었습니다.
책에서는 제라드 메스자로스라는 사람이 말하기를 테스트 대역에는 dummy, stub, spy, fake, mock 라는 다섯 가지가 있습니다. 무려 5가지가 있지만 실제로는 mock과 stub 이 두가지 유형으로 나뉘어진다고 할 수 있습니다.
목은 외부로 나가는 상호 작용을 모방하고 검사하는 데 있고, 스텁은 내부로 들어오는 상호작용을 모방하는 것에 있습니다. 즉 외부에서 데이터를 받아오는 것을 모방하는 것을 스텁, 그리고 코드 동작으로 외부 함수를 실행하거나 API를 보내야 하는 것은 목이라고 나눌 수가 있습니다.
mock은 spy랑 묶이게 되고, stub은 dummy, fake랑 묶이게 됩니다.
앞으로 외부 데이터를 가짜로 만들어서 객체를 만드는 것은 stub 그리고 expect(함수명).calledWith(무엇) 같은 형식은 spy나 mock이라고 부르고 또 그렇게 사용하도록 해야겠다는 생각을 했습니다.
5. 리팩터링할 코드 식별
제품 코드 들은 크게 2차원 x축 y축으로 구분해서 볼 수 있다. 복잡도 또는 도메인 유의성 그리고 협력자 수로 기준을 나눌 수가 있습니다.
첫 번째 축의 도메인 유의성은 문제 도메인에 대해 얼마나 의미를 가지고 있는지를 나타냅니다. 일반적으로 도메인 혹은 비즈니스와 직접적으로 연관이 되어 있는 코드들은 그 의미가 큽니다. 반면 공통 라이브러리 같은 유틸 함수들은 비즈니스와는 거리가 멀기 떄문에 유의성이 낮습니다.
두 번째 축의 협력자 수는 테스트를 하고자 하는 대상의 클래스 또는 메서드가 가지고 있는 다른 협력자 수 입니다. 즉 다른 함수를 import 하거나 클래스를 import하는 숫자가 이 협력자의 숫자를 높이게 하는 것 입니다.
다음으로 우리의 코드의 종류를 살펴 봅니다. 우리 제품 코드들은 크게 4가지로 나뉘어진다고 볼 수 있습니다.
- 도메인 모델과 알고리즘: 보통 복잡한 코드는 주로 도메인 모델이긴 하지만 보통은 도메인 모델은 다른 도메인과 분리를 하는 것이 원칙입니다. 알고리즘은 보통 도메인과 연관이 되어 있는 알고리즘이 많습니다. 그래서 이러한 종류의 코드는 주로 협력자 수는 적지만 그 의미는 매우 크다고 볼 수 있습니다.
- 간단한 코드: 간단한 코드는 정말 간단한 유틸 함수 들을 말하게 됩니다. 협력자는 거의 없는 경우가 많고 도메인에 대한 유의성도 굉장이 낮은 코드들 입니다.
- 컨트롤러: 비즈니스 로직에 직접적으로 참여하기 보다는 단순히 어디서 받아온 것을 어디로 연결한다는 개념이 강합니다. 라우팅 컨트롤러, 그리고 외부 API나 함수를 부르는 코드들이 속하게 됩니다. 이런 코드는 비즈니스 로직과 멀기 때문에 유의성은 가지고 있지 않지만, 협력자 수가 굉장히 많은 특징을 가지고 있습니다.
- 지나치게 복잡한 코드: 마지막으로 지나치게 복잡한 코드는 우리가 흔히 봤던 한 컴포넌트에 몇 천줄 되는 코드라인을 가지고 있는 것을 말합니다. 즉 호출하는 외부 API도 많고, 모든 비즈니스 로직도 같이 뒤섞여서 만들어진 컴포넌트들이 이런 부류에 속하게 됩니다.
사실 여기서 리팩터링을 해야하는 대상은 누가봐도 4 번째 요소긴 합니다. 만약에 4번 요소에 테스트코드를 추가 하거나 테스트를 넣어보려고 하는 시도를 하려고 하거든, 키보드에서 손을 떼셔야 합니다. 책에서는 4번 같은 코드에 테스트 코드를 짜는 것은 테스트를 짜지 않은 것만 못하다고 합니다. 이런 부분까지 커버하면서 커버리지 100%를 맞추고자 하는 행위는 정말 무의미한 테스트 코드이니 4번의 요소가 발견 되면 분리를 먼저 해야만 합니다.
그럼 이 코드를 테스트 친화적으로 리팩터링 하려면 3번과 1번으로 분리할 수 있도록 작성을 하면 됩니다. 즉 도메인 모델과 외부 호출 API 같은 것을 분리해서 작성하는 것이 이론상 리팩터링 하는 방법입니다.
5 - 1. 험블 객체 패턴을 사용해 분할해보기
말로는 쉽지 실전으로 옮기기가 어렵습니다. 책에서는 제라드 메스자로스가 이야기한 험블 객체 패턴이 가장 실전에 가까운 방법론이라고 이야기를 합니다.
테스트 대상 코드의 로직을 테스트하려면, 테스트가 가능한 부분을 추출하면 됩니다. 그렇게 추출을 하다보면 단일 책임 원칙을 지키는 객체를 만드는 것을 목적으로 분리하고, 그 기준은 비즈니스 로직을 대상으로 합니다.
이렇게 비즈니스 로직만을 대상으로 하면 이제 비즈니스 로직은 모든 것과 분리가 되어 있는 상태가 되고, 런던파의 방식 대로 비즈니스 로직에 붙어 있는 남아 있는 의존성을 전무 mocking 하고, 입력을 넣어주게 되면 우리는 정상적으로 비즈니스 로직이 잘 도는지에 대한 테스트를 할 수 있게 됩니다.
6. 통합테스트
이제 단위로 테스틀 하면서 빠른 피드백을 받으며 개발하면서 단위들의 문제가 없어도 실제로 어플리케이션에서는 문제가 발생할 수 있습니다. 왜냐면 그 어플리케이션의 실행 방향은 현재 산으로 가고 있어도 단위 테스트에서는 알 길이 없기 때문입니다.
이런 부분을 잡아 줄 수 있는 것이 통합테스트입니다. E2E도 그렇지 않냐라고 생각할 수 있는데, 사실 E2E 테스트는 통합테스트의 부분집합이라고 할 수 있기 때문에 여기서는 크게 통합테스트로 갑니다.
사실 어떻게 보면 통합테스트도 단위테스트에 한 종류일 수도 있습니다. 왜냐하면 단위라는 것이 클래스나 함수의 단위일 수도 있지만 아까 2 챕터에서 말한 것 처럼 행동이나 액션도 하나의 단위가 될 수 있기 때문입니다. 하지만 저는 크게 통합테스트와 단위테스트의 큰 차이는 테스트코드를 작성 할 때, 기획서 기반으로 작성하는가? 아니면 코드를 기반으로 작성하는가? 로 나뉘어진다고 생각합니다.
실제로 기획서는 어떻게 유저가 행동해서 어떤 화면을 띄워야 하는가로 작성이 됩니다. 즉 이런 시나리오를 코드로 옮기다 보면 결국에는 통합적으로 테스트를 하는 상황이 만들어지게 됩니다. 이런 경우는 통합테스트라고 생각합니다.
반면 코드는 우리가 어떤 함수나 클래스를 작성할 때, 이것이 과연 내가 생각한데로 리턴을 뱉을까? 혹은 실행하고 나서의 상태 값은 제대로 변경이 됬을까? 라는 마음으로 테스트 코드를 작성하는 경우를 저는 단위테스트라고 생각합니다.
자 그럼 이제 우리는 기획서를 기반으로 유저의 행동 기반으로 테스트를 작성했다는 가정에서 의존성을 찾아 볼 수 있습니다.
책에서는 이런 의존성을 크게 두 가지로 나뉘어집니다. 관리가 가능한 의존성과 관리를 할 수 없는 의존성으로 말입니다.
관리가 가능한 의존성은 애플리케이션과 밀접하고 상호작용하는 부분을 외부 환경에서 볼 수가 없는 것들을 말합니다. 예를 들면 내가 만든 다른 비즈니스 로직 객체, 싱글턴 객체, 어플리케이션 안에서만 도는 라이브러리들이 있습니다. (rxjs, lodash 등)
관리가 불가능한 의존성은 관리가 불가능한 의존성을 말합니다. 관리가 불가능한 의존성은 애플리케이션과 밀접하지 않은 외부로 나가는 것 들이 있습니다. 예를 들면 외부 API 호출, 다른 컴퓨터의 함수 호출 등의 통신을 위주로 하는 것이 여기에 속합니다.
이 책에서 독특한 것은 백엔드를 기준으로 데이터베이스는 가급적 관리가 가능한 의존성에 둔다는 부분에 있습니다. 그래서 만약 데이터베이스를 사용할 수 없을 경우에는 컨트롤러를 테스트 하는 의미가 없고 차라리 도메인 모델의 단위 테스트에만 집중하라고 합니다.
통합 테스트에서 가장 중요한 가치는 결국 마지막에 유저에게 어떻게 나가게 될 것인지 까지 보게 되는 것을 중요한 가치로 보는 것 같습니다.
7. 마치며
사실 얼마전까지만 해도 프론트에서 테스트코드를 짜는 것은 백엔드 보다 훨씬 어렵고, 또 작성을 하더래도 QA를 돌리는 것 만큼 정확하지 않아서 큰 의미가 없지 않을까 라는 생각을 했었습니다.
하지만 최근에 객체지향의 사실과 오해를 읽으면서 객체라는 것은 사실 백엔드보다도 클라이언트에서의 개념이 더 들어가게 된다는 점 부터 시작하며 테스트에 대한 고민까지 하게 되었습니다.
그러면서 cypress를 도입하여 회사에서 적용해보며 느낀 점은 역할과 책임에 따라서 테스트코드를 작성하면, 백엔드보다도 더 쉽게 테스트 코드를 작성할 수 있으며, 심지어 이런 테스트 코드를 작성하는 것이 개발할 때, 직접 눌러보고 입력해야하는 상황을 자동화 할 수 있다는 매력을 느꼈습니다.
그런 와중에 이 책을 읽고, 테스트의 목표, 테스트의 정의, 테스트에서 사용되는 용어, 좋은 테스트를 위한 방법, 테스트 방법에 대한 차이 등을 한번에 얻어갈 수 있었던 책이었던 것 같습니다.
사실 많은 회사에서도 아직 테스트에 대한 부분은 빠른 개발을 해야하는 상황에서는 걸림돌이라고 생각 할 수 있습니다. 미시적으로는 입력 테스트 자동화부터 시작해서 조금씩 도입해보면, 코드를 바라보는 시선이 달라질 것이라고 생각합니다. 좋은 변수명 좋은 함수보다도 테스트가 가능한 코드를 작성하는 것이 유지 보수와 좋은 코드 품질을 유지하는 것에 핵심이라고 생각이 들게 해준 책이었습니다.
'혼자 주저리주저리' 카테고리의 다른 글
객체지향의 사실과 오해 (독후감) (2) | 2022.09.10 |
---|