본문 바로가기

Android/Lint

[Lint] Lint에 Custom rule을 추가해보자. - 5. UnitTest를 통한 Lint 검증

728x90

본 게시글은 이전 게시글에 이어서 작성된 부분입니다.

2022.04.13 - [Android/Lint] - [Lint] Lint에 Custom rule을 추가해보자. - 1. 기본 설정 및 적용

2022.04.15 - [Android/Lint] - [Lint] Lint에 Custom rule을 추가해보자. - 2. XML Rule

2022.04.17 - [Android/Lint] - [Lint] Lint에 Custom rule을 추가해보자. - 3. Code Convention Rule

2022.04.19 - [Android/Lint] - [Lint] Lint에 Custom rule을 추가해보자. - 4. LintFix를 통한 Lint 수정

 

이번 게시글에서는 작성한 Lint를 사용하여 UnitTest를 통해 검증을 해볼 예정이다.

 

사실 해당 과정이 어째서 필요한가? 라고 묻는다면, 명확한 답변을 하긴 어려울 것 같다.

하지만, TDD 기반으로 생각해 본다면 Custom Lint Rule을 추가하는 것 또한 Test 기반으로 작성되어야 하지 않을까? 라는 생각이 든다.

2022.02.27 - [Android/TDD] - [TDD] TDD 란 ?

TDD에 대한 내용은 위의 게시글에 작성되어있다.

 

물론 해당 부분은 Lint에 대한 스터디 중,

오.. Lint도 Test 코드를 작성할 수 있네.. 신기하다..

 

라고 생각이 들어 필자의 흥미 위주로, 따로 간단하게 테스트 코드를 작성하여 적용해 보았고,

그것을 토대로 추후에 사용할 가능성을 생각하여 간단하게나마 적용한 것들을 작성해보고자 한다.


우선,

테스트를 하기 위해서는 Gradle에 test에 관련된 세팅을 해주어야 한다.

 

// test
testImplementation 'junit:junit:4.13.2'
testImplementation "com.android.tools.lint:lint:30.1.3"
testImplementation "com.android.tools.lint:lint-tests:30.1.3"

 

Lint Module의 Gradle에 다음과 같이 testImplementation을 추가해주도록 한다.

 

그 후,

Lint 모듈에 존재하는 Test 디렉토리에 Test Class를 작성해준다.

 

 

필자는 RegistryTest와 DetectorTest 두 가지를 모두 확인해보기 위하여 두개로 나누어 작성하였다.

 

위의 폴더 구조를 보면 알 수 있겠지만,

Lint 모듈의 경우 다른 모듈과 다르게 계측 테스트는 존재하지 않고 로컬 테스트만 존재한다.

뭐, 계측 테스트가 존재한다고 해도 Lint를 어떻게 테스트하나 싶겠지만 말이다.

 

우선 RegistryTest에 대한 내용부터 확인해보도록 하자.

테스트 코드는 기존 다른 모듈에서 진행했던 테스트와 동일하게, 작성해둔 LintIssueRegistry()를 가져와서 설정한 값들을 확인하면 된다.

 

@Test
fun checkUseApiVersion() {
    val apiVersion = LintIssueRegistry().api
    assertEquals(apiVersion, CURRENT_API)
}

@Test
fun testNumberOfIssues() {
    val size = LintIssueRegistry().issues.size
    assertEquals(size, 8)
}

@Test
fun testGetIssues() {
    val issues = LintIssueRegistry().issues
    assertEquals(issues[0], LintLogDetector.ISSUE)
}

 

 

간단하게 테스트 코드를 작성해 보았다.

 

Registry 클래스에서 사용할 수 있는 값이 api와 issue List이기 때문에 이 두 값을 확인할 수 있도록 코드를 작성해 보았다.

checkUseApiVersion() 에서는 Registry에 설정된 api Version이 CURRENT_API와 동일한지,

testNumberOfIssues() 에서는 선언된 Detector List의 크기가 8인지,

testGetIssues() 에서는 0번째 index에 들어간 이슈가 LintLogDetector.ISSUE가 맞는지 확인하는 테스트 코드이다.

 

이렇게 해서 테스트를 돌려보면 당연하다시피 오류가 발생한다.

 

The LintClient.clientName must be initialized before running other lint code
java.lang.IllegalStateException: The LintClient.clientName must be initialized before running other lint code

 

 

무슨 LintClient.clientName이 시작 전에 초기화 되어야 한다고 한다.

 

해당 오류 내용을 토대로 구글링을 해보았을 때, 해당 문제를 발생시킨 사람이 없는지 뭐 나오는 것이 없다.

따라서, LintClient class에서 clientName에 대한 설명을 확인해 보았는데 그냥 아무 값이나 설정해도 상관이 없는 것으로 보인다.

 

var registry: LintIssueRegistry? = null

@Before
fun setUp() {
    LintClient.clientName = "init LintClient Name"
    registry = LintIssueRegistry()
}

 

그래서 @Before 어노테이션을 사용하여 Test가 실행되기 전 세팅을 하도록 구현해 주었고,

이왕 하는김에 LintIssueRegistry 객체를 해당 부분에서 초기화 시켜서 사용할 수 있도록 수정해 주었다.

 

위와 같이 코드를 추가해주고 테스트를 수행하면 정상적으로 모든 값이 통과가 되는 것을 볼 수 있고, 값을 임의로 변경하였을 때 정상적으로 실패하는 것을 확인할 수 있을 것이다.

 

위에 말했다 시피 Registry 클래스의 경우 override되어 사용하고 있는 값이 2가지 밖에 없지만, IssueList에 선언된 값을 사용하여 등록된 Issue에 대하여 테스트 코드를 작성할 수 있다.

 

@Test
fun `check Issue ID - explanation`() {
    val output = LintIssueRegistry().issues
        .joinToString(separator = "\n") { "- ${it.id} - ${it.getExplanation(TextFormat.RAW)}" }

    val issuesData = """
    - Lint-LogDetector - This explanation is sample.
    ...
    """.trimIndent()

    assertEquals(issuesData, output)
}

 

일단 IssueData에 들어가는 내용은 길어지는 관계로 ... 로 표기하였다.

필요하다면 하단의 Github 링크로 들어가 전체 소스코드에서 확인하길 바란다.

 

 

우선 Test함수의 이름부터 뭔가 다른 것을 볼 수 있는데, Lint Test에 대한 내용을 찾아보다가 신기한 함수 명명 방법을 찾아서 한번 적용해 보았다.

작은 따옴표(')를 사용하여 선언한 영역 안에서는 어떤 키워드나 특수문자도 사용이 가능하다.

 

우선, output에 저장되는 값에 대해서 설명하자면, 선언된 Issues list에서 id값과 explanation 값

- id - explanation

와 같은 형태로 저장하며, 각 Issue끼리는 \n 인 줄바꿈으로 구분하여 저장한다.

 

사실, 이렇게 사용하기 보다는 각 Issue에 대한 값을 각각 확인하도록 사용하는 것이 일반적일 것으로 보인다.

 

val issueId = LintIssueRegistry().issues[0].id

 

이와 같이 배열을 사용하여 해당 index에 선언된 Issue에 대한 정보를 가져올 수 있으며, 해당 값을 사용하여 Test 코드를 작성하면 되지 않을까 싶다.

 

 

다음으로,

DetectorTest에 대하여 확인해보자.

 

class CustomLintDetectorTest : LintDetectorTest() {

    override fun getDetector(): Detector {
        TODO("Not yet implemented")
    }

    override fun getIssues(): MutableList<Issue> {
        TODO("Not yet implemented")
    }
}

 

DetectorTest의 경우 LintDetectorTest Class를 상속 받아서 테스트가 가능하다.

LintDetectorTest를 상속받는 경우, getDetector, getIssues 두 가지의 메서드를 override하여 사용해야 하는데, 해당 함수들을 override하여 사용하면, RegistryTest에서 setup했던 것들이나 별도의 작업을 할 필요 없이 바로 Detector에 대한 테스트를 할 수 있어서 편리하다.

 

class CustomLintDetectorTest : LintDetectorTest() {

    override fun getDetector() = LintVariableDetector()

    override fun getIssues() = listOf(LintVariableDetector.ISSUE)

    @Test
    fun testSampleKotlinFile() {
        lint()
            .sdkHome(File("Your SDK Path."))
            .files(
                kotlin(
                    """
                    class TestClass {
                        fun checkMethod() {
                            val sample_test = "123";
                        }
                    }
                    """
                ).indented()
            )
            .run()
            .expectClean()
    }
}

 

우선 필자는 Detector에는 변수에 저장된 값을 체크하는 Detector를 등록하였고, Issues에는 해당 Detector에 선언 된 Issue를 등록하였다.

 

Test함수를 확인해 보면, 지금까지 사용했던 테스트 방식과 좀 다른 모습을 확인할 수 있을 것이다.

 

LintDetectorTest Class를 상속받았기 때문에, TestLintTask를 반환하는 lint() 메서드를 사용할 수 있다.

 

TestLintTask를 사용하여 Detector를 테스트할 수 있는데, 여기서 사용되는 추가적인 메서드는 다음과 같다.

  • sdkHome : SDK가 설치된 위치를 설정한다.
  • files : Detector를 사용해 감지할 File을 선언한다.
  • run : 실행.
  • expectClean : 오류나 에러가 없는지 확인한다.

sdkHome같은 경우, 자신의 로컬에서 sdk가 설치 된 위치를 작성해주면 된다.

 

File > Setting > Android SDK > Android SDK Location

 

해당 경로를 따라가면 어디에 SDK가 설치되어 있는지 알 수 있다.

 

files는 테스트를 진행 할 파일을 선언하는데,

사용하는 방법은 위의 예제 코드를 보면 알 수 있다시피, 사용할 언어(kotlin)를 선언 하고, 그 안에 class를 직접 구현해주면 된다.

마지막의 indented()는 구현한 파일을 규격에 맞춰서 들여쓰기 시켜주는 메서드이다.

 

 

TestFile Class를 확인해보면 알 수 있겠지만, kotlin으로 선언해주면 선언된 string값에 따라서 알맞은 언어의 파일을 create 해준다.

 

또한, 해당 Class의 내용을 확인해보니 Kotlin 외에도 Java, XML도 동일한 방식으로 선언하여 사용이 가능하니, Java를 사용하거나 XML에 대한 Detector를 테스트하고 싶은 경우에는 선언을 바꾸어서 코드를 작성한 후에 테스트를 진행하면 될 것으로 보인다.

 

run은 위에 작성된 파일을 토대로 Lint를 실행하는 메서드이고,

expectClean은 오류나 에러가 없는지 확인 해주는 메서드이다.

 

expectClean 대신에 expect를 사용하여 직업 오류가 발생했을 때 보고되는 메세지를 선언해 줄 수 있는데 해당 부분을 커스텀하여 사용할 필요는 없을 것으로 보여 expectClean으로 사용하였다.

단, expect함수를 어느 하나라도 사용하지 않는 경우 오류나 에러를 잡지 못하여, Lint에 어긋나는 테스트 코드가 작성되어 있어도 감지하지 못하므로 반드시 expect에 관련된 함수를 선언해 주어야 한다.

 

다시 테스트코드로 돌아가서, 해당 테스트 코드를 실행시켜 보면 다음과 같은 결과가 나오게 된다.

 

 

LintVariableDetector에서는 변수 이름에 언더바(_)가 사용되는 경우 Lint를 발생하도록 하였으며, sample_test 라는 변수를 사용했기 때문에 위와 같은 에러가 발생하게 된 것이다.

 

에러 내용을 확인해보면, LintVariableDetector에서 Report할 때 지정한 Message가 출력되는 것을 확인할 수 있다.

따라서 해당 부분에서 언더바를 제거한 후에 다시 테스트를 돌려보면, 정상적으로 통과 되는 것을 확인할 수 있다.


간단하게 Custom Lint에 대한 테스트 코드를 작성해 보았는데,

TDD 기반으로 개발을 할 것이 아니라면 역시 해당 부분까지 구현할 필요는 없지 않을까 라는 생각이 든다.

 

Lint 자체가 코드에서 시각적으로 수정할 부분을 보여주도록 구현한 것인데,

테스트 코드까지 작성하지 않아도 기존 사용하는 코드에서 확인하면 될 것이라고 생각한다.

 

하지만, 빌드하는데 시간이 오래걸리거나 기존 코드를 수정하지 않고 Lint를 추가하여 확인해보고 싶다면 DetectorTest를 만들어서 확인하는 것은 그나마 사용 가능성이 있어 보인다.

필자도 Lint를 적용할 때, 수정하고 빌드해서 확인하고 하는 일련의 과정에서 생각보다 시간을 많이 잡아먹어서 불편하다고 생각했으니 말이다.

 

물론, 위와 같은 이유로 테스트 코드를 작성하는 것은, 본래 테스트 코드를 작성하는 이유와는 조금 거리가 있긴하지만 개발자의 편의를 위해서라면 작성해도 괜찮지 않을까 생각이 든다.

 

해당 게시글에 사용한 예제는 다음 Github에 올려두었다.

https://github.com/HeeGyeong/CleanArchitectureSample

 

GitHub - HeeGyeong/CleanArchitectureSample

Contribute to HeeGyeong/CleanArchitectureSample development by creating an account on GitHub.

github.com

 

728x90