programing

유닛 테스트를 대규모 레거시(C/C++) 코드베이스에 도입하려면 어떻게 해야 합니까?

prostudy 2022. 7. 31. 21:14
반응형

유닛 테스트를 대규모 레거시(C/C++) 코드베이스에 도입하려면 어떻게 해야 합니까?

대규모 멀티플랫폼 어플리케이션이 C로 작성되어 있습니다(소량이지만 C++의 양이 증가하고 있습니다).대규모 C/C++ 어플리케이션에서 기대할 수 있는 많은 기능을 탑재해, 오랜 세월에 걸쳐 진화해 왔습니다.

  • #ifdef지옥.
  • 테스트 가능한 코드를 분리하기 어려운 대용량 파일
  • 너무 복잡해서 쉽게 테스트할 수 없는 기능

이 코드는 임베디드 디바이스를 대상으로 하기 때문에 실제 타깃에서 실행하는 것은 큰 오버헤드가 됩니다.따라서 로컬 시스템에서 빠른 사이클로 개발 및 테스트를 더 많이 수행하고 싶습니다.다만, 「시스템상의 .c 파일에 카피/붙여넣기, 버그 수정, 카피/붙여넣기」라고 하는 기존의 전략은 피하고 싶습니다.개발자가 번거롭게 할 경우 나중에 동일한 테스트를 다시 작성하여 자동으로 실행할 수 있도록 하고 싶습니다.

여기 우리의 문제가 있습니다: 코드를 모듈화 하기 위해서는 테스트성이 향상되어야 합니다.하지만 자동 유닛 테스트를 도입하기 위해서는 모듈화가 필요합니다.

한 가지 문제는 파일이 너무 크기 때문에 적절한 유닛테스트를 작성하기 위해 stub-out이 필요한 함수를 파일 내에 호출하는 함수가 있을 수 있다는 것입니다.코드가 모듈화 되어 문제가 없어질 것 같지만, 아직 멀었습니다.

테스트 가능한 것으로 알려진 소스 코드에 코멘트를 태그 붙이는 것을 검토했습니다.그런 다음 테스트 가능한 코드의 스크립트 스캔 소스 파일을 작성하여 별도의 파일로 컴파일하여 유닛 테스트와 링크할 수 있습니다.결함을 수정하고 기능을 추가함에 따라 유닛 테스트를 서서히 도입할 수 있습니다.

그러나 (필요한 모든 stub 기능과 함께) 이 스킴을 유지하는 것은 너무 번거로운 일이 되어 개발자가 유닛 테스트의 유지보수를 중단할 우려가 있습니다.따라서 다른 접근법은 모든 코드의 stub을 자동으로 생성하는 도구를 사용하여 파일을 그 코드와 링크하는 것입니다.(이것을 가능하게 하는 유일한 툴은 고가의 시판용 제품입니다)그러나, 이 어프로치에서는, 외부 콜만이 삭제될 수 있기 때문에, 개시하기 전에, 모든 코드를 보다 모듈화 할 필요가 있는 것 같습니다.

개인적으로는 개발자가 자신의 외부 종속성에 대해 생각해 보고 자신의 스탭을 지능적으로 작성했으면 합니다.하지만 너무 많이 자란 10,000줄 파일에 대한 의존성을 모두 제거하는 것은 엄청난 일일 수 있습니다.개발자에게 모든 외부 의존관계에 대해 stub를 유지해야 한다고 설득하는 것은 어려울 수 있지만, 이것이 올바른 방법일까요?(다른 한 가지 주장은 서브시스템의 유지보수가 서브시스템의 stub를 유지해야 한다는 것입니다).하지만 개발자들에게 자신의 스탭을 쓰도록 강요하는 것이 더 나은 유닛 테스트로 이어질지 의문입니다.)

#ifdefs물론, 이 문제에 또 다른 전체 차원을 추가합니다.

C/C++ 기반의 유닛 테스트 프레임워크에 대해 몇 가지 살펴보았는데, 문제가 없을 것 같은 옵션이 많이 있습니다.그러나 "유닛 테스트 없는 코드 뭉치"에서 "유닛 테스트 가능한 코드"로 쉽게 전환할 수 있는 방법을 찾지 못했습니다.

이 일을 겪은 다른 분들에게 묻고 싶은 것은 다음과 같습니다.

  • 좋은 출발점은 어디일까요?우리가 올바른 방향으로 가고 있는 건가요, 아니면 분명한 것을 놓치고 있는 건가요?
  • 이행에 도움이 되는 툴은 무엇입니까?(현재의 예산은 거의 「제로」이기 때문에, 지극히 무료/오픈 소스)

참고로 빌드 환경은 Linux/UNIX 기반이기 때문에 Windows 전용 도구는 사용할 수 없습니다.

「유닛 테스트 없는 코드의 헤어볼」에서 「유닛 테스트 가능한 코드」로의 이행을 용이하게 하는 것은 발견되지 않았습니다.

기적적인 해결책은 없고, 단지 수년간 축적된 기술적 부채를 바로잡기 위해 많은 노력을 했을 뿐이라는 것이 얼마나 슬픈 일인가.

쉬운 전환은 없습니다.당신은 크고 복잡한 심각한 문제를 안고 있어요.

아주 작은 단계만으로 해결할 수 있습니다.각 작은 단계에는 다음이 포함됩니다.

  1. 꼭 필요한 코드를 따로따로 고르세요. (고물상에서 가장자리를 갉아먹지 마세요.)중요한 컴포넌트를 선택하면 나머지 컴포넌트에서 어떻게든 잘라낼 수 있습니다.단일 기능이 이상적이지만, 복잡한 함수 클러스터이거나 전체 함수 파일일 수 있습니다.테스트 가능한 컴포넌트에 적합하지 않은 것부터 시작해도 괜찮습니다.

  2. 이게 뭘 하는지 알아내야지인터페이스가 무엇인지 알아내세요.이를 위해 목표 조각을 실제로 이산 상태로 만들기 위해 초기 리팩터링을 수행해야 할 수도 있습니다.

  3. 「전체」통합 테스트를 작성합니다.현재로서는, 검출된 코드의 어느쪽인가를 테스트합니다.중요한 것을 바꾸려고 하기 전에 이것을 통과시키세요.

  4. 코드를 깔끔하고 테스트 가능한 유닛으로 리팩터링하여 현재 헤어볼보다 이치에 맞게 만듭니다.전체적인 통합 테스트에서는 (현재로서는) 하위 호환성을 유지해야 합니다.

  5. 새 유닛에 대한 쓰기 유닛 테스트.

  6. 모든 것이 통과되면 오래된 API를 해제하고 변경으로 인해 파손되는 부분을 수정하십시오.필요한 경우 원래 통합 테스트를 다시 수행합니다. 오래된 API가 테스트되고 새 API가 테스트됩니다.

반복하다.

마이클 페더스는 '레거시 코드로 효과적으로 작동'이라는 성경을 썼다.

레거시 코드와 테스트 도입에 대한 저의 경험은 "특성화 테스트"를 만드는 것에 불과합니다.입력이 알려진 테스트 작성을 시작하고 출력을 가져옵니다.이러한 테스트는 실제로 어떤 기능을 하는지 알 수 없지만 작동 중인 메서드/클래스에 유용합니다.

단, 유닛 테스트(특성화 테스트도)를 작성하는 것이 거의 불가능한 경우가 있습니다.그 경우는, 승인 테스트( 경우는 Fitnesse)를 통해서 문제를 해결합니다.

하나의 기능을 테스트하고 적합성을 확인하는 데 필요한 모든 클래스를 만들 수 있습니다.'특성평가 테스트'와 비슷하지만 한 단계 더 높습니다.

George가 말했듯이, "Legacy Code로 효과적으로 일하는 것"은 이런 종류의 성서이다.

그러나 팀의 다른 사람들은 테스트를 계속하는 것이 그들에게 개인적으로 이득이라고 생각하는 것 밖에 없습니다.

이를 위해서는 가능한 한 사용하기 쉬운 테스트 프레임워크가 필요합니다.다른 개발자를 위한 계획을 세우고, 테스트를 예로 들어 자신의 개발자를 작성합니다.유닛 테스트 경험이 없는 경우, 프레임워크 학습에 시간을 할애할 필요는 없습니다.쓰기 유닛 테스트의 발달이 늦어지는 것을 알 수 있기 때문에 프레임워크를 모르는 것은 테스트를 건너뛰는 핑계입니다.

크루즈 컨트롤, luntbuild, cdash 등을 사용한 지속적인 통합에 시간을 할애합니다.코드가 매일 밤 자동으로 컴파일되고 테스트가 실행되는 경우 유닛 테스트에서 QA 전에 버그가 발견되면 개발자가 이점을 확인할 수 있습니다.

한 가지 권장할 점은 공유 코드 소유권입니다.개발자가 코드를 변경하여 다른 사람의 테스트를 어겼을 경우, 그 사람이 테스트를 수정하기를 기대해서는 안 되며, 테스트가 왜 작동하지 않는지 조사하고 스스로 수정해야 합니다.내 경험상 이것은 성취하기 가장 어려운 것 중 하나이다.

대부분의 개발자는 어떤 형태의 유닛 테스트를 작성하며, 경우에 따라서는 체크인을 하거나 빌드를 통합하지 않는 작은 일회용 코드를 작성하기도 합니다.이것들을 빌드에 간단하게 통합할 수 있도록 하면, 개발자가 구입을 개시할 수 있습니다.

제 접근방식은 새로운 테스트를 추가하는 것입니다.코드가 변경되면 기존 코드를 너무 많이 분리하지 않으면 원하는 수만큼 상세 테스트를 추가할 수 없습니다.실용적인 측면에서 오류가 발생합니다.

유닛 테스트를 고집하는 유일한 장소는 플랫폼 고유의 코드입니다.#ifdef가 플랫폼 고유의 상위 수준의 기능/클래스로 대체되는 경우 모든 플랫폼에서 동일한 테스트를 수행해야 합니다.이를 통해 새로운 플랫폼을 추가하는 데 드는 많은 시간을 절약할 수 있습니다.

boost::test를 사용하여 테스트를 구성하며, 간단한 자체 등록 기능을 통해 쓰기 테스트를 쉽게 수행할 수 있습니다.

이들은 CTest(CMake의 일부)로 랩되어 유닛 테스트 실행 파일 그룹을 한 번에 실행하고 간단한 보고서를 생성합니다.

야간 빌드는 ant 및 lunt build(개미 glues c++, .net 및 Java 빌드)로 자동화됩니다.

조만간 자동 도입과 기능 테스트를 빌드에 추가할 예정입니다.

우리는 정확히 이것을 하는 과정에 있다.3년 전 저는 유닛 테스트도 거의 없고 코드 리뷰도 거의 없으며 꽤 임시 빌드 프로세스도 없는 프로젝트에 개발팀에 합류했습니다.

코드 베이스는 COM 컴포넌트 세트(ATL/MFC), 크로스 플랫폼 C++ Oracle 데이터 카트리지 및 일부 Java 컴포넌트로 구성되어 있으며, 모두 크로스 플랫폼 C++ 코어 라이브러리를 사용합니다.코드 중 일부는 거의 10년이 되었다.

첫 번째 단계는 몇 가지 단위 테스트를 추가하는 것이었습니다.안타깝게도 동작은 매우 데이터 중심적이기 때문에 처음에는 데이터베이스의 테스트 데이터를 사용하는 유닛 테스트 프레임워크(처음에는 CppUnit, 현재는 JUnit 및 NUnit을 사용하여 다른 모듈로 확장)를 생성하는 데 약간의 노력이 있었습니다.초기 테스트의 대부분은 가장 바깥 층을 연습하는 기능 테스트였고 실제로는 단위 테스트가 아니었다.테스트 하니스를 구현하려면 (예산이 필요할 수 있음) 약간의 노력이 필요할 수 있습니다.

유닛 테스트 추가 비용을 최대한 낮게 해주시면 도움이 될 것 같습니다.테스트 프레임워크에 의해 기존 기능의 버그를 수정할 때 테스트를 추가하는 것이 비교적 쉬워졌습니다.새로운 코드에서는 적절한 유닛 테스트를 실시할 수 있습니다.코드의 새로운 영역을 리팩터링 및 실장할 때 코드의 작은 영역을 테스트하는 적절한 유닛테스트를 추가할 수 있습니다.

작년에 NAT은 크루즈 컨트롤과의 지속적인 통합을 추가하고 빌드 프로세스를 자동화했습니다.이것은 초기에 큰 문제였던 시험을 최신 상태로 유지하고 합격시키기 위한 훨씬 더 많은 동기를 부여한다.따라서 개발 프로세스의 일부로 정기적인(적어도 야간에) 유닛 테스트 실행을 포함할 것을 권장합니다.

델은 최근 코드 리뷰 프로세스를 개선하는 데 주력하고 있습니다.이는 매우 드물고 비효율적이었습니다.그 목적은 개발자들이 코드 리뷰를 더 자주 하도록 권장할 수 있도록 코드 리뷰를 시작하고 실행하는 것을 훨씬 더 저렴하게 만드는 것입니다.또, 프로세스 개선의 일환으로서 프로젝트 계획에 포함되는 코드 리뷰나 유닛 테스트의 시간을 단축하는 것에 노력하고 있습니다.기존에는 일정에서 놓치기 쉬운 일정의 시간을 개발자에게 할당하고 있었습니다.

저는 Green field 프로젝트와 오랜 세월에 걸쳐 성장한 대규모 C++ 어플리케이션 및 많은 개발자와 함께 작업해 왔습니다.

솔직히 유닛 테스트와 테스트 우선 개발이 큰 가치를 창출할 수 있는 상태로 레거시 코드 베이스를 가져오려고 애쓰지 않습니다.

레거시 코드 베이스가 일정 크기 및 복잡해지면 유닛 테스트 적용범위에 많은 이점을 가져다 주는 수준까지 도달하는 작업은 완전한 개서 작업에 해당합니다.

주요 문제는 테스트 가능성 리팩터링을 시작하자마자 버그가 발생한다는 것입니다.높은 테스트 범위를 확보해야만 새로운 버그가 발견되어 수정될 수 있습니다.

즉, 매우 느리고 신중하게 진행해야 하며, 몇 년 후에나 제대로 테스트된 코드 베이스의 이점을 얻을 수 있습니다(아마도 합병 등이 발생한 이후로는 결코 얻을 수 없을 것입니다).한편, 소프트웨어의 최종 사용자에게 명백한 가치가 없는 새로운 버그를 도입하고 있을 가능성이 있습니다.

또는 모든 코드에 대한 높은 테스트 범위에 도달할 때까지 빠르게 진행되지만 코드 베이스가 불안정합니다.(그 결과, 2개의 브런치, 1개는 실가동, 1개는 유닛 테스트 버전용)

물론 일부 프로젝트에서는 이 모든 것이 규모의 문제이기 때문에 개서에는 몇 주밖에 걸리지 않을 수 있으며 개서할 가치가 있습니다.

고려해야 할 한 가지 방법은 먼저 통합 테스트 개발에 사용할 수 있는 시스템 전체의 시뮬레이션 프레임워크를 구축하는 것입니다.통합 테스트부터 시작하는 것은 직관에 반하는 것처럼 보일 수 있지만, 귀하가 설명한 환경에서 진정한 단위 테스트를 수행하는 문제는 상당히 심각합니다.소프트웨어 전체 실행 시간을 시뮬레이션하는 것 이상일 수도 있습니다.

이 접근방식은 단순히 열거된 문제를 회피하는 것입니다.다만, 다양한 문제를 얻을 수 있습니다.그러나 실제로는 견고한 통합 테스트 프레임워크를 사용하면 유닛 레벨에서 기능을 발휘하는 테스트를 개발할 수 있습니다.단, 유닛의 분리는 필요 없습니다.

PS: Python 또는 Tcl을 기반으로 하는 명령어 기반 시뮬레이션 프레임워크 작성을 검토하십시오.이렇게 하면 테스트 스크립트를 쉽게 작성할 수 있습니다.

안녕하세요.

예를 들어 헤더 파일에서 dec를 사용하는 등 눈에 띄는 포인트를 살펴보는 것으로 시작합니다.

그럼 암호가 어떻게 배치되어 있는지 살펴보기 시작해요.논리적인가요?큰 파일을 작은 파일로 분할할 수도 있습니다.

Maybe grab a copy of Jon Lakos's excellent book "Large-Scale C++ Software Design" (sanitised Amazon link) to get some ideas on how it should be laid out.

Once you start getting a bit more faith in the code base itself, i.e. code layout as in file layout, and have cleared up some of the bad smells, e.g. using dec's in header files, then you can start picking out some functionality that you can use to start writing your unit tests.

Pick a good platform, I like CUnit and CPPUnit, and go from there.

It's going to be a long, slow journey though.

HTH

cheers,

Its much easier to make it more modular first. You can't really unittest something with a whole lot of dependencies. When to refactor is a tricky calculation. You really have to weigh the costs and risks vs the benefits. Is this code something that will be reused extensively? Or is this code really not going to change. If you plan to continue to get use out of it, then you probably want to refactor.

Sounds like though, you want to refactor. You need to start by breaking out the simplest utilities and build on them. You have your C module that does a gazillion things. Maybe, for example, there's some code in there that is always formatting strings a certain way. Maybe this can be brought out to be a stand-alone utility module. You've got your new string formatting module, you've made the code more readable. Its already an improvement. You are asserting that you are in a catch 22 situation. You really aren't. Just by moving things around, you've made the code more readable and maintainable.

Now you can create a unittest for this broken out module. You can do that a couple of ways. You can make a separate app that just includes your code and runs a bunch of cases in a main routine on your PC or maybe define a static function called "UnitTest" that will execute all the test cases and return "1" if they pass. This could be run on the target.

Maybe you can't go 100% with this approach, but its a start, and it may make you see other things that can be easily broken out into testable utilities.

테스트를 쉽게 사용할 수 있습니다.

"자동으로 실행"하는 것부터 시작합니다.개발자(사용자 포함)가 테스트를 작성하도록 하려면 테스트를 쉽게 실행하고 결과를 확인하십시오.

3행의 테스트를 작성하여 최신 빌드와 대조하여 실행한 후 결과를 확인할 수 있습니다.단 한 번의 클릭만으로 개발자를 커피 머신으로 보낼 수 없습니다.

즉, 최신 빌드가 필요하며 코드 작업 방식 등을 변경해야 할 수 있습니다.이러한 프로세스는 임베디드 디바이스의 PITA가 될 수 있다는 것을 알고 있기 때문에 조언할 수 없습니다.하지만 시험을 보는 것이 어렵다면, 아무도 시험을 치르지 않을 거라는 걸 알아요.

테스트할 수 있는 것을 테스트

여기서 흔히 볼 수 있는 유닛 테스트의 이념에 어긋나는 것은 알지만, 테스트하기 쉬운 것을 쓰기 위한 테스트입니다.저는 조롱을 하지 않고 테스트 가능한 상태로 만들기 위해 리팩터링도 하지 않으며 UI가 포함되어 있다면 유닛 테스트도 하지 않습니다.하지만 점점 더 많은 내 도서관 일상이 그것을 가지고 있다.

나는 간단한 테스트들이 발견하는 경향이 있다는 것에 매우 놀랐다.낮게 매달린 과일을 따는 것은 결코 쓸모없는 일이 아니다.

다른 관점에서 보자: 만약 그것이 성공적인 제품이 아니었다면 당신은 그 거대한 헤어볼을 유지할 계획을 세우지 않았을 것이다.현재의 품질관리는 교환할 필요가 있는 완전한 장애는 아닙니다.오히려 유닛 테스트를 하기 쉬운 곳에서 실시합니다.

(단, 당신은 그것을 끝내야 합니다.빌드 프로세스에서 "모든 것을 수정"하는 데 얽매이지 마십시오.)

코드 베이스를 개선하는 방법을 가르쳐 주세요.

그런 역사를 가진 코드 베이스라면 분명 개선이 필요할 거야하지만 당신은 결코 모든 것을 재작성하지는 못할 것이다.

동일한 기능을 가진 두 개의 코드를 보면 대부분의 사람들은 어떤 것이 특정 측면(퍼포먼스, 가독성, 유지보수성, 테스트성 등)에서 더 나은지에 동의할 수 있습니다.어려운 부분은 세 가지입니다.

  • 다른 측면의 균형을 잡는 방법
  • 이 코드 조각이 충분하다는 것에 동의하는 방법
  • 어떻게 하면 망가뜨리지 않고 충분히 좋은 코드로 바꿀 수 있을까요?

첫 번째 포인트는 아마도 가장 어렵고 공학적인 질문만큼이나 사회적인 질문일 것입니다.하지만 다른 점들은 배울 수 있다.저는 이 접근방식을 채택한 정식 코스는 모릅니다만, 사내에서 뭔가 정리할 수 있을지도 모릅니다.두 남자가 함께 작업하는 것부터, 귀찮은 코드를 가지고 개선 방법을 논의하는 「워크샵」에 이르기까지, 무엇이든 할 수 있습니다.


There is a philosophical aspect to it all.

Do you really want tested, fully functional, tidy code? Is it YOUR objective? Do YOU get any benefit at all from it?.

yes, at first this sounds totally stupid. But honestly, unless you are the actual owner of the system, and not just an employee, then bugs just means more work, more work means more money. You can be totally happy while working on a hairball.

I am just guessing here, but, the risk you are taking by taking on this huge fight is probably much higher than the possible pay back you get by getting the code tidy. If you lack the social skills to pull this through, you will just be seen as a troublemaker. I've seen these guys, and I've been one too. But of course, it's pretty cool if you do pull this through. I would be impressed.

But, if you feel you are bullied into spending extra hours now to keep an untidy system working, do you really think that that will change once the code gets tidy and nice?. No.. once the code gets nice and tidy, people will get all this free time to totally destroy it again at the first available deadline.

in the end it's the management that creates the workplace nice, not the code.

I think, basically you have two of separate Problems:

  1. Large Code base to refactor
  2. Work with a team

Modularization, refactoring, inserting Unit tests and alike is a difficult task, and i doubt that any tool could take over larger parts of that work. Its a rare skill. Some Programmers can do that very well. Most hate it.

Doing such a task with a team is tedious. I strongly doubt that '"forcing" developers' ever will work. Iains thoughts are very well, but I would consider finding one or two programmers who are able to and who want to "clean up" the sources: Refactor, Modualrize, introduce Unit Tests etc. Let these people do the job and the others introduce new bugs, aehm functions. Only people who like that kind of work will succeed with that job.

Not sure is it actual or not, but I have small advice here. As I understand, you ask methodological question about incremental non-invasive integration of unit testing into huge legacy code with a lot of stakeholders protecting their swamp.

Usually, first step is to build your testing code independently from all other code. Even this step in long-live legacy code is very complex. I propose to build your testing code as a dynamic shared library with run-time linking. That will allow you to refactor only small piece of code which is undertesting and not whole 20K file. So, you can start covering function by function without touching/fixing all linking issues

ReferenceURL : https://stackoverflow.com/questions/748503/how-do-you-introduce-unit-testing-into-a-large-legacy-c-c-codebase

반응형