객체를 조롱하기 어려운 시스템을 어떻게 테스트합니까?


34

다음 시스템으로 작업하고 있습니다.

Network Data Feed -> Third Party Nio Library -> My Objects via adapter pattern

우리는 최근에 내가 사용하고있는 라이브러리의 버전을 업데이트 한 문제가 있었는데, 그중에서도 타임 스탬프 (제 3 자 라이브러리가로 반환 long)가 에포크 후 밀리 초에서 에포크 후 나노 초로 변경되었습니다.

문제 :

타사 라이브러리 개체를 모의하는 테스트를 작성하면 타사 라이브러리 개체에 대해 실수를 한 경우 테스트가 잘못 됩니다. 예를 들어 타임 스탬프의 정밀도가 변경되어 모의 데이터가 잘못 반환되어 단위 테스트를 변경해야한다는 사실을 알지 못했습니다. 이것은 라이브러리의 버그아니며 문서에서 무언가를 놓 쳤기 때문에 발생했습니다.

문제는 실제 데이터 피드없이 실제 데이터를 생성 할 수 없기 때문에 이러한 데이터 구조에 포함 된 데이터를 확신 할 수 없다는 것입니다. 이 객체는 크고 복잡하며 많은 다른 데이터 조각이 있습니다. 타사 라이브러리에 대한 설명서가 잘못되었습니다.

질문:

이 동작을 테스트하기 위해 테스트를 어떻게 설정합니까? 테스트 자체가 쉽게 잘못 될 수 있기 때문에 단위 테스트 에서이 문제를 해결할 수 있는지 확실하지 않습니다. 또한 통합 시스템은 크고 복잡하며 쉽게 놓칠 수 있습니다. 예를 들어, 위 상황에서 타임 스탬프 처리를 여러 곳에서 올바르게 조정했지만 그 중 하나를 놓쳤습니다. 시스템은 통합 테스트에서 대부분 올바른 작업을 수행하는 것처럼 보였지만 더 많은 데이터가있는 프로덕션에 시스템을 배포 할 때 문제가 분명해졌습니다.

현재 통합 테스트를위한 프로세스가 없습니다. 테스트는 본질적으로 : 단위 테스트를 양호하게 유지하고, 문제가 발생할 때 더 많은 테스트를 추가 한 다음 테스트 서버에 배포하고, 제정신이 아닌지 확인한 다음 프로덕션에 배포하십시오. 이 타임 스탬프 문제는 모의가 잘못 작성되어 단위 테스트를 통과 한 다음 즉각적이고 명백한 문제를 일으키지 않았기 때문에 통합 테스트를 통과했습니다. 품질 관리 부서가 없습니다.


3
실제 데이터 피드를 "기록"하여 나중에 타사 라이브러리에 "재생"할 수 있습니까?
Idan Arye

2
누군가 이런 문제에 관한 책을 쓸 수 있습니다. 실제로 Michael Feathers는 다음과 같은 책을 썼습니다. c2.com/cgi/wiki?WorkingEffectivelyWithLegacyCode 그는 코드를보다 테스트 가능하게 만들기 위해 어려운 종속성을 깨뜨리는 여러 가지 기술을 설명합니다.
cbojar

2
타사 라이브러리 주변의 어댑터? 예, 그것이 제가 추천하는 바로 그 것입니다. 이러한 단위 테스트는 코드를 개선하지 않습니다. 더 안정적이거나 유지 관리가 용이하지 않습니다. 그 시점에서 다른 사람의 코드를 부분적으로 복제하는 것입니다. 이 경우 잘못 작성된 코드를 사운드에서 복제합니다. 순 손실입니다. 일부 답변은 일부 통합 테스트를 제안합니다. "이것이 효과가 있습니까?"를 원한다면 좋은 생각입니다. 위생 검사. 좋은 테스트는 어렵고 좋은 코드만큼의 기술과 직관력이 필요합니다.
jpmc26

4
내장의 악의 완벽한 그림. 왜 라이브러리는 반환하지 않습니다 Timestamp(라는 이름의 방법 (그들이 원하는 표현을 포함한) 클래스 및 제공 .seconds(), .milliseconds(), .microseconds(), .nanoseconds()) 물론라는 이름의 생성자를. 그러면 아무런 문제가 없었을 것입니다.
Matthieu M.

2
"코딩의 모든 문제는 간접적 인 계층에 의해 해결 될 수있다 (물론, 너무 많은 간접적 인 계층의 문제는 제외)"라는 생각이 떠오른다.
Dan Pantry

답변:


27

이미 실사를하고있는 것 같습니다. 그러나 ...

가장 실용적인 수준에서는 항상 자신의 코드에 맞게 스위트에 "풀 루프"통합 테스트를 모두 포함하고 필요한 것보다 많은 어설 션을 작성하십시오. 특히, 전체 작성-읽기-[do_stuff]-유효성 검증주기를 수행하는 소수의 테스트가 있어야합니다.

[TestMethod]
public void MyFormatter_FormatsTimesCorrectly() {

  // this test isn't necessarily about the stream or the external interpreter.
  // but ... we depend on them working how we think they work:
  var stream = new StreamThingy();
  var interpreter = new InterpreterThingy(stream);
  stream.Write("id-123, some description, 12345");

  // this is what you're actually testing. but, it'll also hiccup
  // if your 3rd party dependencies introduce a breaking change.
  var formatter = new MyFormatter(interpreter);
  var line = formatter.getLine();
  Assert.equal(
    "some description took 123.45 seconds to complete (id-123)", line
  );
}

이미 이런 일을하고있는 것 같습니다. 벗겨 지거나 복잡한 라이브러리를 다루고 있습니다. 이 경우 라이브러리에 대한 이해를 확인하고 라이브러리 사용 방법의 예제로 사용되는 몇 가지 "라이브러리 작동 방식"유형의 테스트를 수행하는 것이 좋습니다.

JSON 구문 분석기가 JSON 문자열의 각 "유형"을 해석하는 방법 을 이해 하고 의존 해야한다고 가정하십시오 . 스위트에 다음과 같은 것을 포함시키는 것이 도움이되고 사소합니다.

[TestMethod]
public void JSONParser_InterpretsTypesAsExpected() {
  String datastream = "{nbr:11,str:"22",nll:null,udf:undefined}";
  var o = (new JSONParser()).parse(datastream);

  Assert.equal(11, o.nbr);
  Assert.equal(Int32.getType(), o.nbr.getType());
  Assert.equal("22", o.str);
  Assert.equal(null, o.nll);
  Assert.equal(Object.getType(), o.nll.getType());
  Assert.isFalse(o.KeyExists(udf));
}

그러나 두 번째로, 모든 종류의 거의 모든 수준의 자동화 된 테스트는 여전히 모든 버그로부터 사용자를 보호하지 못한다는 점을 기억하십시오. 문제를 발견 할 때 테스트를 추가하는 것이 일반적 입니다. 품질 관리 부서가 없으면 최종 사용자가 이러한 문제를 많이 발견 할 수 있습니다.

그리고 상당한 정도로, 그것은 정상입니다.

셋째, 라이브러리가 필드 또는 메소드의 이름을 바꾸거나 종속 코드를 변경하지 않고 반환 값 또는 필드의 의미를 변경하면 해당 유형을 변경하여 어쩌면 그 출판사에 불만을 느낍니다. 그리고 변경 로그가있을 경우 변경 로그를 읽어야했지만 게시자에게 스트레스를 전달해야한다고 주장합니다. 나는 그들이 희망적으로 건설적인 비판이 필요하다고 주장합니다 ...


자, 라이브러리에 JSON 문자열을 공급하는 것만 큼 간단했으면 좋겠다. 그렇지 않습니다. 실제 구문 분석을 수행하는 모든 클래스와 패키지 (new JSONParser()).parse(datastream)에서 직접 데이터를 가져 오기 때문에 의 동등한 작업을 수행 할 수 없습니다 NetworkInterface.
durron597

또한 변경 내역에는 타임 스탬프가 ms에서 ns로 변경되었다는 사실이 포함되지 않았으며, 문서화하지 않은 다른 두통들도 포함되었습니다. 그렇습니다, 나는 그들에게 매우 만족스럽고, 나는 이것을 그들에게 표현했습니다.
durron597

@ durron597 아, 거의 없습니다. 그러나 첫 번째 코드 예제와 같이 기본 데이터 소스를 위조 할 수 있습니다. ... 요점은 : 가능하면 풀 루프 통합 테스트를 수행하고, 가능하면 라이브러리에 대한 이해를 테스트하고, 여전히 버그를 야생에 빠뜨릴 것임을 알고 있어야합니다 . 또한 타사 공급 업체 눈에 보이지 않고 변경을 수행 할 책임이 있습니다.
svidgen

@ durron597 익숙하지 않습니다 NetworkInterface... 인터페이스를 localhost의 포트에 연결하여 데이터를 공급할 수 있습니까?
svidgen

NetworkInterface. 네트워크 카드로 직접 작업하고 소켓을 여는 등의 저수준 객체입니다.
durron597

11

짧은 대답 : 어렵습니다. 당신은 아마 좋은 대답이없는 것처럼 느껴질 것입니다. 그것은 쉬운 대답이 없기 때문입니다.

긴 대답 : @ptyx가 말했듯이 단위 테스트뿐만 아니라 시스템 테스트 및 통합 테스트가 필요합니다.

  • 단위 테스트는 빠르고 쉽습니다. 개별 코드 섹션에서 버그를 발견하고 모의를 사용하여 실행할 수있게합니다. 필요에 따라 코드 조각 (밀리 초 대 나노초 등)간에 불일치를 포착 할 수 없습니다.
  • 통합 테스트 및 시스템 테스트는 속도가 느리고 실행이 어렵지만 더 많은 오류가 발생합니다.

몇 가지 구체적인 제안 :

  • 시스템 테스트를 가능한 한 많은 시스템에서 실행하도록하는 것의 이점이 있습니다. 비록 그것이 많은 행동을 검증 할 수 없거나 문제를 정확히 찾아 낼 수는 없습니다. (Micheal Feathers는 레거시 코드를 효과적으로 사용하는 방법 에 대해 자세히 설명합니다 .)
  • 테스트 가능성에 투자하면 도움이됩니다. 있다 거대한 여기에 사용할 수있는 기술의 수 : 지속적인 통합, 스크립트, VM을, 도구가 재생, 프록시 또는 리디렉션 네트워크 트래픽.
  • 테스트 가능성에 대한 투자의 장점 중 하나는 분명하지 않을 수 있습니다. 테스트가 지루하거나 성가 시거나 성가 시거나 작성하기가 번거 롭다면 압력을 받으면 건너 뛰기가 너무 쉽습니다. 또는 피곤하다. 테스트를 "이 작업을 수행하지 않을 이유가 없습니다 "임계 값 이하로 유지하는 것이 중요합니다.
  • 완벽한 소프트웨어는 실현 가능하지 않습니다. 다른 모든 것들과 마찬가지로 테스트에 소요되는 노력은 트레이드 오프이며 때로는 노력할 가치가 없습니다. QA 부서 부족과 같은 제약 조건이 있습니다. 버그 발생, 복구 및 학습을 수락하십시오.

프로그래밍은 문제 및 솔루션 공간에 대한 학습 활동으로 설명되었습니다. 미리 모든 것을 완벽하게하는 것이 실현 가능하지 않을 수도 있지만 사실을 알게 된 후에는 배울 수 있습니다. ( "여러 곳에서 타임 스탬프 처리를 수정했지만 하나를 놓쳤습니다. 타임 스탬프 처리를보다 명확하고 놓치기 어렵게하거나 중앙 집중화하여 한 곳만 변경할 수 있도록 데이터 형식이나 클래스를 변경할 수 있습니까? 타임 스탬프 처리에 대한 더 많은 측면을 검증하기위한 테스트, 향후 테스트 환경을 단순화하여 더 쉽게 만들 수 있을까요, 더 쉽게 만들 수있는 도구를 상상할 수 있습니까? 그렇다면 Google에서 그러한 도구를 찾을 수 있습니까? "등)


7

나는 도서관의 버전을 업데이트했다 long.

이것은 라이브러리의 버그아닙니다

나는 당신과 동의하지 않습니다. 이 라이브러리의 버그 , 사실 오히려 음험 한. 반환 값의 의미 유형을 변경했지만 반환 값의 프로그래밍 형식은 변경하지 않았습니다. 이 방법은 모든 종류의 혼란을 초래할 수 있습니다. 특히 이것이 작은 버전의 충돌 일지라도 주요한 충돌 일 경우에도 마찬가지입니다.

대신 라이브러리 MillisecondsSinceEpoch가을 보유한 간단한 래퍼 유형을 반환했다고 가정 해 봅시다 long. 그들이 그것을 NanosecondsSinceEpoch값으로 변경했을 때 , 당신의 코드는 컴파일에 실패했을 것이고, 당신이 변경해야 할 곳을 분명히 지적했을 것입니다. 변경 사항으로 인해 프로그램이 자동으로 손상되지 않았습니다.

더 좋은 방법 TimeSinceEpoch#toLongNanoseconds메소드와 함께 메소드 를 추가하는 등 더 정밀한 인터페이스를 추가 할 수 있으므로 인터페이스를 조정할 수있는 객체 일 것입니다 #toLongMilliseconds.

다음 문제는 라이브러리에 대한 신뢰할 수있는 통합 테스트 세트가 없다는 것입니다. 그것들을 써야합니다. 라이브러리 주위에 인터페이스를 만들어서 응용 프로그램의 나머지 부분에서 캡슐화하는 것이 좋습니다. 다른 답변들도이 문제를 해결했습니다 (그리고 계속 입력하면 계속 팝업됩니다). 통합 테스트는 단위 테스트보다 덜 자주 실행해야합니다. 이것이 버퍼 레이어를 갖는 것이 도움이되는 이유입니다. 통합 테스트를 별도의 영역으로 분리하거나 이름을 다르게 지정하여 단위 테스트를 실행할 때마다 필요에 따라 실행할 수 있습니다.


2
@ durron597 나는 여전히 버그라고 주장합니다. 문서 부족을 넘어서서 예상되는 행동을 전혀 바꾸지 않는 이유는 무엇입니까? 왜 새로운 정밀도를 제공하는 새로운 방법을 사용하고 이전 방법이 여전히 밀리 초를 제공하게합니까? 그리고 컴파일러가 반환 유형 변경을 통해 경고하는 방법을 제공하지 않는 이유는 무엇입니까? 문서뿐만 아니라 코드 자체에서 이것을 훨씬 명확하게 만드는 데 많은 시간이 걸리지 않습니다.
cbojar

1
@gbjbaanb, "그들이 가난한 릴리스 관행을 가지고는"나에게 버그 것 같아
아르투로 토레스 산체스

2
@gbjbaanb 타사 라이브러리는 사용자와 "계약"을 만들어야합니다. 문서화 여부에 관계없이 계약을 위반하면 버그로 간주 될 수 있습니다. 다른 사람들이 말했듯이, 무언가 변경 해야하는 경우 새로운 기능 / 방법으로 계약에 추가 ...Ex()하십시오 (Win32API의 모든 방법 참조 ). 이것이 가능하지 않은 경우, 기능 (또는 반환 유형)의 이름을 변경하여 계약을 "중단"하는 것이 동작을 변경하는 것보다 낫습니다.
TripeHound

1
이것은 라이브러리의 버그입니다. 오랫동안 나노초를 사용하는 것이 그것을 추진하고 있습니다.
Joshua

1
@gbjbaanb 예상치 못한 경우에도 의도 된 동작이므로 버그가 아니라고 말합니다. 그런 의미에서 그것은 구현 버그가 아니지만 같은 버그입니다. 디자인 결함 또는 인터페이스 버그 라고 할 수 있습니다 . 결함은 명시 적 단위가 아닌 긴 단위로 원시적 인 집착을 드러내고 내부 구현의 세부 정보를 내 보내면 (데이터가 특정 단위의 긴 단위로 저장 됨) 추상화가 누설된다는 사실에 있습니다. 미묘한 단위 변경으로 가장 놀랍게 된 원리.
cbojar

5

통합 및 시스템 테스트가 필요합니다.

단위 테스트는 코드가 예상대로 작동하는지 확인하는 데 유용합니다. 아시다시피, 그것은 당신의 가정에 도전하거나 기대가 제정신을 유지하는 데 아무런 영향을 미치지 않습니다.

제품이 외부 시스템과 거의 상호 작용하지 않거나 잘 알려져 있고 안정적이며 문서화 된 시스템과 상호 작용하지 않으면 자신있게 조롱 할 수 있습니다 (실제로는 거의 발생하지 않음)-단위 테스트로는 충분하지 않습니다.

테스트 수준이 높을수록 예상치 못한 결과로부터 사용자를 보호 할 수 있습니다. 비용 (편의성, 속도, 취성 등)이 발생하므로 단위 테스트는 테스트의 기초를 유지해야하지만, 결국에는 작은 부분의 인간 테스트를 포함한 다른 계층이 필요합니다. 아무도 생각하지 않은 바보 같은 것들.


2

가장 좋은 방법은 최소한의 프로토 타입을 만들고 라이브러리의 작동 방식을 이해하는 것입니다. 그렇게하면 문서가 열악한 라이브러리에 대한 지식을 얻게됩니다. 프로토 타입은 해당 라이브러리를 사용하고 기능을 수행하는 최소한의 프로그램 일 수 있습니다.

그렇지 않으면 요구 사항이 반 정도이고 시스템에 대한 이해가 부족한 상태에서 단위 테스트를 작성하는 것은 의미가 없습니다.

특정 문제에 관해서는 잘못된 메트릭 사용에 대한 요구 사항 변경으로 처리합니다. 문제를 인식하면 단위 테스트와 코드를 변경하십시오.


1

인기 있고 안정적인 라이브러리를 사용하는 경우 라이브러리가 불쾌하게 작동하지 않을 것이라고 가정 할 수 있습니다. 그러나 당신이 묘사 한 것과 같은 것들이이 라이브러리에서 발생한다면, 이것은 분명히 하나가 아닙니다. 이 나쁜 경험을 한 후에,이 라이브러리와의 상호 작용에서 무언가 잘못 될 때마다 실수를 한 가능성뿐만 아니라 라이브러리가 실수를 한 가능성을 조사해야합니다. 이 라이브러리는 "확실하지 않은"라이브러리라고 가정하겠습니다.

우리가 "확실하지 않은"라이브러리에 사용 된 기술 중 하나는 시스템과 해당 라이브러리 사이에 중간 계층을 구축하는 것입니다.이 계층은 라이브러리가 제공하는 기능을 추상화하고 라이브러리에 대한 우리의 기대가 옳았으며 크게 단순화합니다. 미래에 우리는 그 라이브러리를 부팅하고 더 나은 동작을하는 다른 라이브러리로 교체하기로 결정해야합니다.


이것은 실제로 질문에 대답하지 않습니다. 이미 시스템과 라이브러리를 분리하는 계층이 있지만 문제는 라이브러리가 경고없이 변경 될 때 추상화 계층에 "버그"가있을 수 있다는 것입니다.
durron597

1
@ durron597 그러면 레이어가 나머지 응용 프로그램에서 라이브러리를 충분히 격리하지 못할 수 있습니다. 해당 계층을 테스트하는 데 어려움을 겪고 있다면 동작을 단순화하고 기본 데이터를 더욱 강력하게 격리해야 할 수도 있습니다.
cbojar

@cbojar가 말한 것. 또한 위의 텍스트에서 눈에 띄지 않을 수있는 것을 반복하겠습니다. assert키워드 (또는 사용중인 언어에 따라 기능 또는 기능)는 친구입니다. 단위 / 통합 테스트에서 어설 션에 대해 이야기하는 것이 아니라, 격리 계층이 어설 션으로 매우 무거워 야하며 라이브러리의 동작에 대해 주장 할 수있는 모든 것을 주장합니다.
Mike Nakis

이러한 어설 션은 프로덕션 실행시 반드시 실행되는 것은 아니지만 테스트 중에 실행하여 격리 계층에 대한 화이트 박스보기를 제공하므로 계층에서 라이브러리에서 수신 한 정보를 (가능한 한 많이) 확인할 수 있습니다. 소리입니다.
Mike Nakis
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.