Program Tip

대규모 레거시 (C / C ++) 코드베이스에 단위 테스트를 어떻게 도입합니까?

programtip 2020. 10. 24. 11:41
반응형

대규모 레거시 (C / C ++) 코드베이스에 단위 테스트를 어떻게 도입합니까?


C로 작성된 대규모 다중 플랫폼 애플리케이션이 있습니다. (작지만 증가하는 C ++ 포함) 대규모 C / C ++ 애플리케이션에서 기대할 수있는 많은 기능으로 수년에 걸쳐 발전해 왔습니다.

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

이 코드는 임베디드 장치를 대상으로하기 때문에 실제 대상에서 실행하는 데 많은 오버 헤드가 발생합니다. 따라서 로컬 시스템에서 빠른 주기로 개발 및 테스트를 더 많이 수행하고 싶습니다. 그러나 우리는 "시스템의 .c 파일에 복사 / 붙여 넣기, 버그 수정, 복사 / 붙여 넣기"라는 고전적인 전략을 피하고 싶습니다. 개발자가 문제를 해결하려는 경우 나중에 동일한 테스트를 다시 생성하고 자동화 된 방식으로 실행할 수 있기를 바랍니다.

여기에 우리의 문제가 있습니다. 코드를 더 모듈화되도록 리팩토링하려면 더 테스트 가능해야합니다. 그러나 자동화 된 단위 테스트를 도입하려면 더 모듈화되어야합니다.

한 가지 문제는 파일이 너무 크기 때문에 좋은 단위 테스트를 만들기 위해 스텁해야하는 동일한 파일에서 함수를 호출하는 함수가 파일 내에있을 수 있다는 것입니다. 코드가 더 모듈화됨에 따라 이것이 문제가되지 않을 것 같지만 이는 먼 길입니다.

우리가 생각한 한 가지는 "테스트 가능한 것으로 알려진"소스 코드에 주석을 달아주는 것이 었습니다. 그런 다음 테스트 가능한 코드에 대한 스크립트 스캔 소스 파일을 작성하고 별도의 파일로 컴파일 한 다음 단위 테스트와 연결할 수 있습니다. 결함을 수정하고 더 많은 기능을 추가하면서 단위 테스트를 천천히 도입 할 수 있습니다.

그러나이 체계 (필요한 모든 스텁 기능과 함께)를 유지하는 것이 너무 번거롭고 개발자가 단위 테스트 유지를 중단 할 것이라는 우려가 있습니다. 따라서 또 다른 접근 방식은 모든 코드에 대한 스텁을 자동으로 생성하는 도구를 사용하여 파일을 링크하는 것입니다. (우리는이 작업을 수행 할 것이다 발견하는 유일한 도구는 비싼 상용 제품) 그러나이 방법은 보인다 요구하는 모든 코드는 외부 통화가 밖으로 스텁 할 수 있기 때문에 우리는 심지어 시작하기 전에 더 모듈화 할 수있다.

개인적으로 저는 개발자가 외부 종속성에 대해 생각하고 지능적으로 자신의 스텁을 작성하도록하고 싶습니다. 그러나 이것은 끔찍하게 자란 10,000 줄 파일에 대한 모든 종속성을 제거하기에는 압도적 일 수 있습니다. 개발자가 모든 외부 종속성에 대해 스텁을 유지해야한다고 설득하는 것은 어려울 수 있지만 이것이 올바른 방법입니까? (내가 들었던 또 다른 주장은 서브 시스템의 관리자가 서브 시스템의 스텁을 유지해야한다는 것입니다.하지만 개발자가 자신의 스텁을 작성하도록 "강제"하는 것이 더 나은 단위 테스트로 이어질지 궁금합니다.)

#ifdefs물론, 문제를 다른 모든 차원을 추가합니다.

우리는 여러 C / C ++ 기반 단위 테스트 프레임 워크를 살펴 보았고 괜찮아 보이는 많은 옵션이 있습니다. 그러나 우리는 "단위 테스트가없는 코드"에서 "단위 테스트 가능한 코드"로 쉽게 전환 할 수있는 방법을 찾지 못했습니다.

그래서 여기에 이것을 겪은 다른 사람에게 내 질문이 있습니다.

  • 좋은 출발점은 무엇입니까? 올바른 방향으로 가고 있습니까, 아니면 분명한 것을 놓치고 있습니까?
  • 전환에 도움이되는 도구는 무엇입니까? (현재 예산이 거의 "0"이므로 무료 / 오픈 소스가 바람직합니다.)

빌드 환경은 Linux / UNIX 기반이므로 Windows 전용 도구를 사용할 수 없습니다.


우리는 "단위 테스트가없는 코드"에서 "단위 테스트 가능한 코드"로 쉽게 전환 할 수있는 방법을 찾지 못했습니다.

기적적인 해결책이 아니라 수년간 축적 된 기술 부채를 바로 잡기 위해 많은 노력을 기울이는 것이 얼마나 슬픈 일입니까 ?

쉬운 전환은 없습니다. 크고 복잡하며 심각한 문제가 있습니다.

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

  1. 절대적으로 필수적인 개별 코드를 선택하십시오. (쓰레기에서 가장자리를 조금씩 갉아 먹지 마십시오.) 중요하고 어떻게 든 나머지 부분에서 조각 할 수있는 구성 요소를 선택하십시오. 단일 함수가 이상적이지만 얽힌 함수 클러스터 또는 전체 함수 파일 일 수 있습니다. 테스트 가능한 구성 요소에 대해 완벽하지 않은 것으로 시작하는 것은 괜찮습니다.

  2. 무엇을해야하는지 알아 내십시오. 인터페이스가 무엇인지 파악하십시오. 이를 위해 타겟 조각을 실제로 분리하기 위해 초기 리팩토링을 수행해야 할 수 있습니다.

  3. 현재로서는 발견 된대로 개별 코드 조각을 테스트하는 "전체"통합 테스트를 작성하십시오. 중요한 것을 변경하기 전에 이것을 통과 시키십시오.

  4. 코드를 현재 헤어볼보다 더 잘 이해할 수있는 깔끔하고 테스트 가능한 단위로 리팩터링합니다. 전체 통합 테스트와 (당분간) 이전 버전과의 호환성을 유지해야합니다.

  5. 새 단위에 대한 단위 테스트를 작성합니다.

  6. 모든 것이 통과되면 이전 API를 폐기하고 변경으로 인해 손상 될 부분을 수정하십시오. 필요한 경우 원래 통합 테스트를 재 작업하십시오. 이전 API를 테스트하고 새 API를 테스트하려고합니다.

반복합니다.


Michael Feathers는 이것에 대한 성경을 썼습니다. 레거시 코드로 효과적으로 작업


레거시 코드와 테스트 도입에 대한 나의 작은 경험은 " 특성화 테스트 " 를 만드는 것 입니다. 알려진 입력으로 테스트 생성을 시작한 다음 출력을 얻습니다. 이 테스트는 실제로 무엇을하는지 모르지만 작동하고 있다는 것을 알고있는 메서드 / 클래스에 유용합니다.

그러나 때때로 단위 테스트 (특성화 테스트 포함)를 만드는 것이 거의 불가능한 경우가 있습니다. 이 경우 수용 테스트 ( 이 경우 Fitnesse)통해 문제를 공격합니다 .

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


George가 레거시 코드로 효과적으로 일하는 것이 이런 종류의 성경이라고 말했듯이.

그러나 팀의 다른 사람들이 구매할 수있는 유일한 방법은 테스트를 계속 진행하는 것이 개인적으로 이익을 얻는 경우입니다.

이를 위해서는 가능한 한 사용하기 쉬운 테스트 프레임 워크가 필요합니다. 다른 개발자가 자신의 테스트를 예제로 작성하여 직접 작성하도록 계획하십시오. 단위 테스트 경험이없는 경우 프레임 워크를 배우는 데 시간을 소비 할 것으로 기대하지 마십시오. 단위 테스트 작성이 개발 속도를 늦추는 것으로 간주되므로 프레임 워크를 모르는 것은 테스트를 건너 뛸 수있는 변명입니다.

순항 제어, luntbuild, cdash 등을 사용하여 지속적인 통합에 시간을 투자하십시오. 코드가 매일 밤 자동으로 컴파일되고 테스트가 실행되면 개발자는 단위 테스트가 qa 전에 버그를 포착하면 이점을보기 시작합니다.

장려해야 할 한 가지는 공유 코드 소유권입니다. 개발자가 자신의 코드를 변경하고 다른 사람의 테스트를 중단하는 경우 해당 사용자가 테스트를 수정하기를 기 대해서는 안됩니다. 테스트가 작동하지 않는 이유를 조사하고 직접 수정해야합니다. 제 경험상 이것은 달성하기 가장 어려운 일 중 하나입니다.

대부분의 개발자는 어떤 형태의 단위 테스트를 작성하며, 때로는 빌드를 체크인하거나 통합하지 않는 작은 일회용 코드를 작성합니다. 이를 빌드에 쉽게 통합하면 개발자가 구매를 시작할 것입니다.

내 접근 방식은 새로운 테스트를 추가하고 코드가 수정됨에 따라 기존 코드를 너무 많이 분리하지 않고 원하는만큼 또는 세부적인 테스트를 추가 할 수없는 경우가 있습니다.

단위 테스트에 대해 내가 주장하는 유일한 곳은 플랫폼 별 코드입니다. #ifdefs가 플랫폼 별 상위 수준 함수 / 클래스로 대체되는 경우 동일한 테스트로 모든 플랫폼에서 테스트해야합니다. 이렇게하면 새 플랫폼을 추가하는 데 많은 시간이 절약됩니다.

우리는 boost :: test를 사용하여 테스트를 구성하고 간단한 자체 등록 기능을 사용하여 테스트를 쉽게 작성할 수 있습니다.

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

야간 빌드는 ant 및 luntbuild로 자동화됩니다 (ant는 C ++, .net 및 Java 빌드를 붙임).

곧 자동 배포 및 기능 테스트를 빌드에 추가하고 싶습니다.


우리는 정확히 이것을하는 과정에 있습니다. 3 년 전 저는 단위 테스트, 코드 검토가 거의없고 임시 빌드 프로세스가없는 프로젝트에서 개발 팀에 합류했습니다.

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

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

단위 테스트를 추가하는 데 드는 비용을 최대한 낮추면 많은 도움이됩니다. 테스트 프레임 워크를 사용하면 기존 기능의 버그를 수정할 때 테스트를 비교적 쉽게 추가 할 수 있으며 새 코드는 적절한 단위 테스트를 가질 수 있습니다. 새로운 코드 영역을 리팩터링하고 구현할 때 훨씬 작은 코드 영역을 테스트하는 적절한 단위 테스트를 추가 할 수 있습니다.

작년에 우리는 CruiseControl과의 지속적인 통합을 추가하고 빌드 프로세스를 자동화했습니다. 이것은 테스트를 최신 상태로 유지하고 통과하는 데 훨씬 더 많은 인센티브를 추가하며 초기에는 큰 문제였습니다. 따라서 개발 프로세스의 일부로 정기적 (적어도 야간) 단위 테스트 실행을 포함하는 것이 좋습니다.

우리는 최근에 상당히 드물고 비효율적 인 코드 검토 프로세스를 개선하는 데 집중했습니다. 의도는 코드 검토를 시작하고 수행하는 것을 훨씬 저렴하게 만들어 개발자가 더 자주 수행하도록 권장하는 것입니다. 또한 프로세스 개선의 일환으로 개별 개발자가 이에 대해 더 많이 생각해야하는 방식으로 훨씬 낮은 수준에서 프로젝트 계획에 포함 된 코드 검토 및 단위 테스트를위한 시간을 확보하려고하지만 이전에는 고정 된 비율 만있었습니다. 일정에서 길을 잃기 훨씬 쉬 웠던 그들에게 바쳐진 시간.


나는 완전히 단위 테스트를 거친 코드베이스와 수년에 걸쳐 많은 다른 개발자들과 함께 성장한 대규모 C ++ 애플리케이션으로 그린 ​​필드 프로젝트에서 작업했습니다.

솔직히, 단위 테스트와 테스트 첫 개발이 많은 가치를 더할 수있는 상태로 레거시 코드베이스를 얻으려는 시도를 귀찮게하지 않을 것입니다.

레거시 코드베이스가 특정 크기와 복잡성에 도달하면 단위 테스트 적용 범위가 많은 이점을 제공하는 지점까지 도달하면 전체 재 작성과 동일한 작업이됩니다.

주요 문제는 테스트 가능성을 위해 리팩토링을 시작하자마자 버그를 도입하기 시작한다는 것입니다. 높은 테스트 범위를 얻은 후에야 모든 새로운 버그가 발견되고 수정 될 것으로 기대할 수 있습니다.

즉, 매우 천천히 조심스럽게 진행하고 몇 년 후까지 단위 테스트를 거친 코드 기반의 이점을 얻지 못합니다. (합병 등이 발생한 이후로 아마 없을 것입니다.) 그동안 당신은 아마도 소프트웨어의 최종 사용자에게 명백한 가치가없는 몇 가지 새로운 버그를 소개하고있을 것입니다.

또는 빠르게 진행하지만 모든 코드의 높은 테스트 범위에 도달 할 때까지 코드 기반이 불안정합니다. (따라서 생산 단계에 하나씩, 단위 테스트 버전에 대해 하나씩 2 개의 브랜치로 끝납니다.)

일부 프로젝트의 경우이 모든 것이 규모의 문제이므로 재 작성하는 데 몇 주가 걸리며 그만한 가치가 있습니다.


고려해야 할 한 가지 접근 방식은 먼저 통합 테스트를 개발하는 데 사용할 수있는 시스템 전체 시뮬레이션 프레임 워크를 배치하는 것입니다. 통합 테스트로 시작하는 것은 직관적이지 않은 것처럼 보일 수 있지만 설명하는 환경에서 실제 단위 테스트를 수행하는 데 따른 문제는 상당히 큽니다. 아마도 소프트웨어에서 전체 런타임을 시뮬레이션하는 것 이상일 것입니다.

This approach would simply bypass your listed issues -- although it would give you many different ones. In practice though, I've found that with a robust integration testing framework you can develop tests that exercise functionality at the unit level, although without unit isolation.

PS: Consider writing a command-driven simulation framework, maybe built on Python or Tcl. This will let you script tests quite easily...


G'day,

I'd start by having a look at any obvious points, e.g. using dec's in header files for one.

Then start looking at how the code has been laid out. Is it logical? Maybe start breaking large files down into smaller ones.

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.


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.


Make using tests easy.

I'd start with putting the "runs automatically" into place. If you want developers (including yourself) to write tests, make it easy to run them, and see the results.

Writing a test of three lines, running it against the latest build and seeing the results should be only one click away, and not send the developer to the coffe machine.

This means you need a latest build, you may need to change policies how people work on code etc. I know that such a process can be a PITA with embedded devices, and I can't give any advice with that. But I know that if running the tests is hard, noone will write them.

Test what can be tested

I know I run against common Unit Test philosophy here, but that's what I do: Write tests for the things that are easy to test. I don't bother with mocking, I don't refactor to make it testable, and if there is UI involved i don't have a unit test. But more and more of my library routines have one.

I am quite amazed what simple tests tend to find. Picking the low hanging fruits is by no means useless.

Looking at it in another way: You wouldn't plan to maintain that giant hairball mess if it wasn't a successful product. You current quality control isn't a total failure that needs to be replaced. Rather, use unit tests where they are easy to do.

(You need to get it done, though. Don't get trapped into "fixing everything" around your build process.)

Teach how to improve your code base

Any code base with that history screams for improvements, that's for sure. You will never refactor all of it, though.

Looking at two pieces of code with the same functionality, most people can agree which one is "better" under a given aspect (performance, readability, maintainability, testability, ...). The hard parts are three:

  • how to balance the different aspects
  • how to agree that this piece of code is good enough
  • how to turn bad code into good enough code without breaking anything.

The first point is probably the hardest, and as much a social as an engineering question. But the other points can be learned. I don't know any formal courses that take this approach, but maybe you can organize something in-house: anything from two guys worting together to "workshops" where you take a nasty piece of code and discuss how to improve it.



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.

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

반응형