소프트웨어 테스트 프로젝트에서 단위 테스트를 잘 수행하는 방법

머리말

"유닛 테스트"라는 책에서 언급했듯이 학습 단위 테스트는 좋아하는 테스트 프레임워크, 모의 라이브러리 등과 같은 기술 수준에 머물지 않아야 합니다. 계속 열심히 노력하십시오 단위 테스트에 투자한 시간에 대한 수익을 최대화하고 테스트에 투입한 노력을 최소화하며 테스트가 제공하는 이점을 최대화하는 것은 쉽지 않습니다.

일상적인 개발에서 직면하는 문제와 마찬가지로 언어를 배우고 방법을 습득하는 것은 어려운 것이 아니라 투자한 시간에 대한 수익을 극대화하는 것입니다. 단위 테스트는 기본 지식과 프레임워크가 많고 구글에 검색하면 많이 나옵니다. 모범 사례 방법론도 많이 있습니다. 일상 업무에서 이 무기.

단위 테스트의 정의

단위 테스트 란 무엇입니까? 바이두에서

단위 테스트는 소프트웨어에서 테스트 가능한 가장 작은 단위를 확인하고 검증하는 것을 말합니다. [단위]의 의미는 일반적으로 Java의 단위가 클래스를 나타내는 등 실제 상황에 따라 구체적인 의미를 결정해야 합니다.

인간의 관점에서 단위 테스트는 클래스의 정확성을 확인하는 테스트입니다. 통합 테스트 및 시스템 테스트와 다릅니다. 개발자가 주도하는 프런트 엔드 최소 테스트입니다.

일부 학자들은 통계를 통해 다음과 같은 수치를 도출하기도 했습니다.

결함의 85%는 코드 설계 단계에서 발생합니다 .

·  버그 발견 단계가 늦어질수록 비용은 기하급수적으로 증가합니다.

이러한 관점에서 단위 테스트 코드 작성은 전달 품질과 인건비에 매우 중요한 영향을 미칩니다.

흔한 실수

시간 낭비 및 개발 속도에 영향

프로젝트마다 개발 및 테스트 시간 곡선이 다르기 때문에 코드의 수명 주기, 디버깅 능력, 일반적으로 문제가 있는 코드를 검토하는 데 소요되는 시간을 종합적으로 고려해야 합니다. 프로젝트가 진행됨에 따라 이러한 시간이 늘어나게 되는데, 작성한 코드를 영원히 사용하고 작성한 내용에 대해 사람들이 불평하는 것을 방지하려면 단위 테스트가 매우 필요합니다.

테스팅은 테스팅의 일이어야 한다

개발은 가장 먼저 코드를 담당하는 사람이고, 코드에 가장 익숙한 사람이 설계 단계에서 단위 테스트를 편집하므로 보다 자신 있게 전달할 수 있을 뿐만 아니라 테스트 문제 발생을 줄일 수 있습니다. 동시에 자신의 풀 스택 능력도 향상되었습니다.

내가 코드를 작성하지 않았어, 이해할 수 없어

이전 코드는 이해하기 어렵거나 CR이 부족하다고 종종 불평합니다. 사실 단위 테스트를 작성하는 과정도 CR과 학습의 과정이며, 코드의 주요 흐름, 경계, 예외 등을 깊이 이해하고 있습니다. 동시에 코드 명세, 로직, 디자인을 스스로 점검하는 과정이기도 하다. 리팩토링에서 단위 테스트를 작성하고 서로를 보완하는 단위 테스트에서 리팩토링을 작성하는 것이 좋습니다.

좋은 단위 테스트를 작성하는 방법

방법론적으로 공기처럼 느껴지지 않는 AIR의 원리, 즉 Automatic(자동화), Independent(독립), Repeatable(반복 가능)이 있습니다.

내 개인적인 이해는 다음과 같습니다.

1. CI 통합을 통한 자동 운영으로 단위 테스트가 자동으로 실행될 수 있도록 하며 단위 테스트의 검증 결과를 인쇄 출력 대신 assert를 통해 확인합니다. 수동 개입 없이 단위 테스트가 자동으로 실행될 수 있는지 확인합니다.

2. 단위 테스트는 독립적이어야 하고 서로 호출할 수 없으며 종속적인 순서를 가질 수 없습니다. 패키지는 각 테스트 사례 간에 독립적임을 보장합니다.

3. 운영 환경, 데이터베이스, 미들웨어 등의 영향을 받지 않습니다. 단위 테스트를 작성할 때 외부 종속성을 모의 처리해야 합니다.

적용 범위 사양 측면에서 Alibaba와 업계 모두에 많은 표준이 있습니다.

문 적용률은 70%에 달하고 핵심 모듈의 문 적용률과 분기 적용률은 모두 100%에 도달합니다. --- "알리바바 자바 개발 매뉴얼"

단일 테스트 커버리지 등급 참조

레벨 1: 정상적인 프로세스를 사용할 수 있습니다. 즉, 기능이 올바른 매개변수를 입력하면 올바른 출력을 갖게 됩니다.

Level2: 예외 프로세스는 논리 예외를 던질 수 있습니다. 즉, 입력 매개변수가 잘못된 경우 시스템 예외를 던질 수 없지만 자체적으로 정의한 논리 예외를 사용하여 호출 코드의 상위 계층에 오류를 알릴 수 있습니다.

레벨 3: 극한 사례 및 경계 데이터를 사용할 수 있으며 입력 매개변수의 경계 조건을 별도로 테스트하여 출력이 올바르고 유효한지 확인해야 합니다.

레벨 4: 모든 분기와 루프의 논리가 통하고, 테스트할 수 없는 프로세스가 있을 수 없습니다.

레벨 5: 출력 데이터의 모든 필드 검증, 복잡한 데이터 구조의 출력을 위해 각 필드가 올바른지 확인

위 발췌문에서 진술 커버리지와 브랜치 커버리지 모두 수치적, 방법론적 요구 사항이 있는데 실제 작업에서의 관행은 무엇입니까?

저자는 한 분기에 거의 100%에 도달한 작업에서 제출된 코드의 포괄적인 증분 커버리지 비율을 가지고 있었습니다. 내 경험과 연습에 대해 이야기할 수 있습니다.

60% 정도의 단일 테스트 커버리지율은 쉽게 달성할 수 있지만 95% 이상의 커버리지율을 달성하기 위해서는 다양한 코드 분기와 예외, 심지어 구성 및 Bean 초기화 방법까지 커버해야 합니다. 거대하지만 한계 효과 감소. toString을 테스트하고 싶습니다. getter/setter와 같은 메서드도 의미가 없습니다. 얼마가 적당한지, 정해진 기준은 없는 것 같아요. 높은 코드 커버리지 비율은 성공을 의미하지 않으며 높은 코드 품질을 의미하지도 않습니다. 버려야 할 부분은 과감히 무시한다.

모범 사례

이 타이틀은 약간의 타이틀 파티입니다. 단위 테스트와 관련된 수많은 책과 기사가 있습니다. 제 소위 "모범 사례"는 알리의 실제 작업에서 내가 밟은 구덩이 중 일부이거나 개인적으로 중요하다고 생각하는 몇 가지 중요한 사항입니다. 실수가 있으면 , 토론에 오신 것을 환영합니다.

1. 숨겨진 테스트 경계 값

public ApiResponse<List<Long>> getInitingSolution() {
      List<Long> solutionIdList = new ArrayList<>();
      SolutionListParam solutionListParam = new SolutionListParam();
      solutionListParam.setSolutionType(SolutionType.GRAPH);
      solutionListParam.setStatus(SolutionStatus.INIT_PENDING);
      solutionListParam.setStartId(0L);
      solutionListParam.setPageSize(100);
      List<OperatingPlan> operatingPlanList =  operatingPlanMapper.queryOperatingPlanListByType(solutionListParam);
      for(; !CollectionUtils.isEmpty(operatingPlanList);){
          /*
              do something
              */
          solutionListParam.setStartId(operatingPlanList.get(operatingPlanList.size() - 1).getId());
          operatingPlanList =  operatingPlanMapper.queryOperatingPlanListByType(solutionListParam);
      }
      return ResponsePackUtils.packSuccessResult(solutionIdList);
  }

위 코드에 대한 단위 테스트를 작성하는 방법은 무엇입니까?

당연히 단위 테스트를 작성할 때 데이터베이스 쿼리를 모의하고 정보를 찾을 것입니다. 그러나 쿼리 내용이 100을 초과하면 for 루프가 한 번 들어가기 때문에 jacoco의 자동 커버리지로 찾을 수 없습니다. 사실 이 경계 조건은 다루지 않고 개발자의 습관을 통해서만 이러한 경계 조건을 처리할 수 있습니다.

이러한 숨겨진 경계 값을 처리하는 방법 개발자는 통합 테스트 또는 코드 CR에 의존할 수 없으며 향후 유지 관리 담당자가 함정에 빠지지 않도록 단위 테스트를 작성할 때 이를 고려해야 합니다.

2. @Transactional을 사용하지 말고 springboot 테스트에서 실제 데이터베이스를 작동하십시오.

단위 테스트의 컨텍스트는 깨끗해야 하며 트랜잭션 설계의 원래 의도는 통합 테스트를 위한 것입니다(스프링 공식 웹 사이트에서 소개됨).

DAO 계층의 정확성 검증은 DB를 직접 운영하는 것이 더 쉽지만, 오프라인 데이터베이스의 더티 데이터로 오염되기 쉬워 단일 테스트 실패로 이어집니다. 작성자는 데이터베이스에 직접 연결된 단일 테스트 코드를 접하곤 했으며 종종 5분 동안 코드를 변경하고 1시간 동안 데이터베이스의 더티 데이터를 정리했습니다. 두 번째는 통합 테스트가 전체 애플리케이션의 컨테이너를 시작해야 하므로 효율성을 향상시키려는 원래 의도에 위배됩니다.

정말로 DAO 레이어의 정확성을 테스트하고 싶다면 H2 임베디드 데이터베이스를 통합할 수 있습니다. 많은 온라인 자습서가 있으므로 여기에서 반복하지 않겠습니다.

3. 단일 시험의 시간 관련 내용

저자는 직장에서 극단적인 경우를 접한 적이 있다.평소에는 CI가 정상적으로 실행되다가 밤늦게 공개되자 CI가 실행되지 않는 현상이 발생했다. 단일 테스트 로직에는 CI를 통과하지 못하게 하는 야간 로직(야간 메시지가 전송되지 않음)이 포함되어 있습니다. 단일 테스트에서 시간을 처리하는 방법은 무엇입니까?

Mockito를 사용하는 경우 mock(Date.class)를 사용하여 날짜 개체를 시뮬레이트한 다음 when(date.getTime()).thenReturn(time)을 사용하여 날짜 개체의 시간을 설정할 수 있습니다.

calendar.getInstance()를 사용하는 경우 어떻게 현재 시간을 얻습니까? Calendar.getInstance()는 정적 메서드이며 Mockito에서 조롱할 수 없습니다. 다음을 지원하려면 powerMock을 도입하거나 mockito 4.x로 업그레이드해야 합니다.

@RunWith(PowerMockRunner.class)
  @PrepareForTest({Calendar.class, ImpServiceTest.class})   
  public class ImpServiceTest {
      @InjectMocks
      private ImpService impService = new ImpServiceImpl();
      @Before
      public void setup(){
          MockitoAnnotations.initMocks(this);
          Calendar now = Calendar.getInstance();
          now.set(2022, Calendar.JULY, 2 ,0,0,0);
          PowerMockito.mockStatic(Calendar.class);
          PowerMockito.when(Calendar.getInstance()).thenReturn(now);
      }
  }

4. 최종 클래스, 정적 클래스 등에 대한 단위 테스트

3번 항목에서 언급한 달력 예제와 같이 정적 클래스의 모의에는 mockito4.x 버전이 필요합니다. 그렇지 않으면 powermock을 도입해야 합니다. Powermock은 mockito 3.x 및 mockito 4.x와 호환되지 않습니다. 이전 응용 프로그램은 많은 mockito3.x 버전을 도입했기 때문에 mockito4.x를 직접 사용하여 최종 및 정적 클래스를 모방하려면 패키징이 필요합니다. 실제로 [url=] JUnit [/url], Mockito, Powermock은 버전 번호 간 호환성 문제가 있어 java.lang.NoSuchMethodError가 발생할 수 있으므로 실제 상황에 따라 mocking을 위한 버전을 선택해야 합니다.

하지만 새로운 프로젝트가 수립되면 사용할 mockito 및 junit의 버전을 결정하고 환경이 안정적이고 사용 가능한지 확인하기 위해 powermock과 같은 프레임워크를 도입할지 여부를 결정해야 합니다. 오래된 프로젝트의 경우 mockito 및 powermock의 버전을 대규모로 변경하지 않는 것이 좋습니다. 패키지 정리 및 수명 의심이 쉽기 때문입니다.

5. 애플리케이션이 시작되고 Can not load this fake sdk class 예외를 보고합니다.

Ali의 tair와 metaq는 판도라 컨테이너를 기반으로 하고, fake-sdk는 기본적으로 판도라 모듈 클래스에 의해 로드되기 때문입니다. 특정 원리는 다음 그림을 참조할 수 있습니다.

 

솔루션 1, pandoraboot 환경을 소개합니다.

@RunWith(PandoraBootRunner.class)

이것은 실제로 단일 테스트의 실행 속도를 늦추고 효율성 원칙을 위반합니다. 그러나 전체 컨테이너를 실행하는 것과 비교할 때 판도라 컨테이너의 실행 시간은 약 10초로 여전히 허용 가능합니다.

pandoraboot가 부팅되는 것을 방지하는 순수한 모의 방법이 있습니까? 나는 개인적으로 모의가 ut보다 더 중요하다고 생각합니다. 특히 종종 마이그레이션되거나 오프라인인 일부 외부 종속성은 코드 1줄을 변경할 수 있으며 1시간의 테스트 사례를 복구해야 합니다. Tair, lindorm 및 기타 미들웨어는 로컬 환경을 조롱할 방법이 없으며 외부 리소스에 직접 의존하는 것은 매우 우아하지 않습니다.

솔루션 2, 직접 모의

tair를 예로 들어 보겠습니다.

@RunWith(PowerMockRunner.class)
  @PrepareForTest({DataEntry.class})
  public class MockTair {
      @Mock
      private DataEntry dataEntry;
      @Before
      public void hack() throws Exception {
          //solve it should be loaded by Pandora Container. Can not load this fake sdk class. please refer to http://gitlab.alibaba-inc.com/mi ... dora-boot/wikis/faq for the solution
          PowerMockito.whenNew(DataEntry.class).withNoArguments().thenReturn(dataEntry);
      }

      @Test
      public void mock() throws Exception {
          String value = "value";
          PowerMockito.when(dataEntry.getValue()).thenReturn(value);
          DataEntry tairEntry = new DataEntry();
          //值相等
          Assert.assertEquals(value.equals(tairEntry.getValue()));
      }
  }

6. metaq에서 단일 테스트를 작성하는 방법

MessageExt의 mock 메소드는 5번을 참고하되, 단일 테스트에서 MetaPushConsumer 빈을 실행하고 리스너 메소드를 호출하는 방법은. 그런 다음 컨텍스트의 컨텍스트만 시작할 수 있습니다. SpringRunner를 호스트하는 방법.

@RunWith(PandoraBootRunner.class)
  @DelegateTo(SpringRunner.class)
  public class EventProcessorTest {
      @InjectMocks
      private EventProcessor eventProcessor;
      @Mock
      private DynamicService dynamicService;
      @Mock
      private MetaProducer dynamicEventProducer;
      @Test
      public void dynamicDelayConsumer() throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
          //获取bean
          MetaPushConsumer metaPushConsumer = eventProcessor.dynamicEventConsumer();

          //获取Listener
          MessageListenerConcurrently messageListener = (MessageListenerConcurrently)metaPushConsumer.getMessageListener();
          List<MessageExt> list = new ArrayList<>();

          //这个需要依赖PandoraBootRunner
          MessageExt messageExt = new MessageExt();
          list.add(messageExt);
          Event event = new Event();
          event.setUserType(3);
          String text = JSON.toJSONString(event);
          messageExt.setBody(text.getBytes());
          messageExt.setMsgId(""+System.currentTimeMillis());

          //测试consumeMessage方法
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
          doThrow(new RuntimeException()).when(dynamicService).triggerEventV2(any());
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
          messageExt.setBody(null);
          messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
      }
  }

컨테이너 사용 시기를 요약하면 다음과 같습니다.

// 1. 使用PowerMockRunner
  @RunWith(PowerMockRunner.class)
  // 2.使用PandoraBootRunner, 启动pandora,使用tair,metaq等
  @RunWith(PandoraBootRunner.class)
  // 3. springboot启动,加入context上下文,可以直接获取bean
  @SpringBootTest(classes = {TestApplication.class})

7. ioc를 사용해보세요

IOC를 사용하면 개체를 분리하여 테스트를 더 쉽게 수행할 수 있습니다. 특정 도구 클래스가 특정 서비스에서 사용되는 경우가 종종 있는데, 이 도구 클래스의 메서드는 모두 정적인데, 이 경우 서비스를 테스트할 때 도구 클래스와 함께 테스트해야 합니다.

예를 들어, 다음 코드:

@Service
  public class LoginServiceImpl implements LoginService{
      public Boolean login(String username, String password,String ip) {
          // 校验ip
          if (!IpUtil.verify(ip)) {
              return false;
          }
          /*
            other func
          */
          return true;
      }
  }

IpUtil을 통해 로그인한 사용자의 ip 정보를 확인하고, 이렇게 사용한다면 격리 원칙에 위배되는 IpUtil의 방식을 테스트해야 한다. 로그인 방법을 테스트하려면 도구 클래스 코드를 커버하기 위해 더 많은 테스트 데이터 세트를 추가해야 하며 결합도가 너무 높습니다.

약간 수정된 경우:

 @Service
  public class LoginServiceImpl implements LoginService{
      public Boolean login(String username, String password,String ip) {
          // 校验ip
          if (!IpUtil.verify(ip)) {
              return false;
          }
          /*
            other func
          */
          return true;
      }
  }

이런 식으로 IpUtil 클래스와 LoginServiceImpl 클래스만 별도로 테스트하면 됩니다. LoginServiceImpl을 테스트할 때 IpUtil의 구현을 격리하는 IpUtil을 모의하는 것으로 충분합니다.

8. 커버리지를 위해 무의미한 코드를 테스트하지 마십시오.

예를 들어 getter 및 setter와 같은 toString은 모두 기계 생성 코드이며 단일 테스트는 의미가 없습니다. 전체 테스트 커버리지 개선을 위한 것이라면 CI에서 패키지의 이 부분을 제외하십시오.

 9. void 메서드 테스트 방법

· void 방식이 insertPlan(Plan plan) 등 데이터베이스에 변화를 일으키고 데이터베이스가 H2를 통해 운영되었다면 데이터베이스의 항목 수 변화를 확인하여 void 방식의 정확성을 검증할 수 있다. .

·  void 메서드가 함수를 호출하는 경우 verify 확인 메서드를 통해 호출 수를 얻을 수 있습니다.

userService.updateName(1L,"qiushuo");
  verify(mockedUserRepository, times(1)).updateName(1L,"qiushuo");

·  void 메소드로 인해 예외가 발생할 수 있는 경우.

모의 메서드에 의해 발생한 예외는 dothrow에 의해 조롱될 수 있습니다.

@Test(expected = InvalidParamException.class)
  public void testUpdateNameThrowExceptionWhenIdNull() {
     doThrow(new InvalidParamException())
        .when(mockedUserRepository).updateName(null,anyString();
     userService.updateName(null,"qiushuo");
  }

마지막으로 제 글을 읽어주신 모든 분들께 감사드립니다 호혜는 항상 필요합니다 아주 귀한 것은 아니지만 필요하시면 가져가셔도 됩니다:

이 재료는 [소프트웨어 테스팅] 친구를 위한 가장 포괄적이고 완전한 준비 창고가 될 것입니다.이 창고는 또한 수만 명의 테스트 엔지니어와 함께 가장 어려운 여정을 통과했으며 도움이 되기를 바랍니다! 파트너는 아래 작은 카드를 클릭할 수 있습니다. 받다 

추천

출처blog.csdn.net/OKCRoss/article/details/131379002