ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [nGrinder]스크립트(Groovy) 작성법
    서버 2016. 5. 21. 12:09

    스크립트(Groovy)를 작성하는 법을 조사하기 전, 먼저 Groovy가 무엇인지를 설명한 다음 스크립트 작성법, 스크립트로 테스트 설정 페이지를 수정할 수 있는지, 없으면 nGrinder 소스 조사를 설명하겠습니다.

    Groovy란?


    Groovy는 자바에 파이썬, 루비, 스몰토크등의 특징을 더한 동적 객체 지향 프로그래밍 언어입니다.  JVM에서 동작하고 자바의 강점 위에서 파이썬, 루비, 스몰토크 등의 프로그래밍 언어에 영향을 받은 특징 및 장점이 있습니다. 자바 기반이기 때문에 자바 프로그래머들이 많은 학습을 하지 않아도 프로그래밍을 할 수 있다는 점과 단순화된 문법을 지원하여 코드를 읽고 유지보수하기 편하다는 장점이 있습니다. 

    자바와의 비교

    그루비의 문법체계는 자바를 계승하고 발전시켰습니다. 자바에 없는 간편 표기법을 지원하는 것 외에 LIST, MAP, 정규식을 위한 구문을 제공함으로써 프로그래밍을 쉽고 간결하게 해줍니다. JVM상에서 동작하는 동적 스크립트 언어인 jython, jruby등과 비교해도 손색이 없습니다. 자바는 소스를 컴파일해야만 사용할 수 있지만, 그루비 소스는 스크립트 파일 그대로 실행시킬 수 있고 자바초럼 컴파일하여 사용할 수도 있습니다. 대부분의 자바 소스는 파일 확장자만 변경하면 수정 없이 그루비에서도 사용할 수 있습니다.

    다음은 자바 소스입니다.

    public class StdJava
    {
      public static void main(String argv[])
      {
        for (String it : new String [] {"Rod", "Carlos", "Chris"})
          if (it.length() <= 4)
            System.out.println(it);
      }
    }

    이러한 소스를 그루비에서는 간단하게 다음과 같이 표현할 수 있습니다.

    ["Rod", "Carlos", "Chris"].findAll{it.size() <= 4}.each{println it}

    스크립트(Groovy)작성법


    테스트를 실행할 수 있는 스크립트는 Groovy, Jython, Groovy Maven Project 총 3가지가 있지만 이 중 Groovy를 중심으로 설명하겠습니다.

    Groovy 등장 배경

    Groovy는 nGrinder 3.2부터 지원했습니다. Groovy 스크립트는 Jython 스크립트와 다르게 JUnit 기반으로 동작하도록 되어 있습니다. nGrinder 개발에 참여한 NHN 개발자들의 대부분이 JUnit을 사용한 적이 있고 대부분의 IDE에서 JUnit을 지원하기 때문에 개발을 했다는 얘기가 있습니다. Groovy Maven Project는 Groovy 스크립트에 Maven 구조를 더한 형태입니다. 만약 Maven 구조 없이 단독으로 Groovy 스크립트형태로 작성했다면, nGrinder에서 제공하는 스크립트 에디터에서 편집 검증을 해야 하지만, Groovy Maven Project 구조를 사용했다면 이클립스같은 개발환경으로 해당 프로젝트를 import 후 로컬에서 테스트를 작성할 수 있습니다.

    nGrinder Groovy 스크립트 기본 생성 소스

    먼저 Groovy 스크립트를 생성하거나, Groovy Maven Project에 묶어서 사용해도 스크립트 자체의 형태는 아래와 같이 동일합니다.

    package org.ngrinder;
    import static net.grinder.script.Grinder.grinder
    import static org.junit.Assert.*
    import static org.hamcrest.Matchers.*
    import net.grinder.plugin.http.HTTPRequest
    import net.grinder.plugin.http.HTTPPluginControl;
    import net.grinder.script.GTest
    import net.grinder.script.Grinder
    import net.grinder.scriptengine.groovy.junit.GrinderRunner
    import net.grinder.scriptengine.groovy.junit.annotation.BeforeProcess
    import net.grinder.scriptengine.groovy.junit.annotation.BeforeThread
    // import static net.grinder.util.GrinderUtils.* // You can use this if you're using nGrinder after 3.2.3
    import org.junit.Before
    import org.junit.BeforeClass
    import org.junit.Test
    import org.junit.runner.RunWith
    import HTTPClient.HTTPResponse
    import HTTPClient.NVPair
    /**
     * A simple example using the HTTP plugin that shows the retrieval of a
     * single page via HTTP. 
     * 
     * This script is automatically generated by ngrinder.
     * 
     * @author admin
     */
    @RunWith(GrinderRunner)
    class TestRunner {
    	public static GTest test
    	public static HTTPRequest request
    	@BeforeProcess
    	public static void beforeProcess() {
    		HTTPPluginControl.getConnectionDefaults().timeout = 6000
    		test = new GTest(1, "Test1")
    		request = new HTTPRequest()
    		test.record(request);
    		grinder.logger.info("before process.");
    	}
    	@BeforeThread 
    	public void beforeThread() {
    		grinder.statistics.delayReports=true;
    		grinder.logger.info("before thread.");
    	}
    	@Test
    	public void test(){
    		HTTPResponse result = request.GET("http://please_modify_this.com")
    		if (result.statusCode == 301 || result.statusCode == 302) {
    			grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode); 
    		} else {
    			assertThat(result.statusCode, is(200));
    		}
    	}
    }

    nGrinder Groovy 스크립트 내의 클래스에는 JUnit을 nGrinder에 맞게 처리할 수 있도록 @RunWith(Grinder Runner)를 사용합니다.

    @RunWith(): 스프링의 테스트 컨텍스트 프레임워크의 JUnit 확장기능 지정

    • JUnit은 각각의 테스트가 서로 영향을 주지 않고 독립적으로 실행하는 것을 기본으로 하기에 각 테스트 클래스마다 매번 오브젝트를 생성합니다. 그러므로 각 테스트 클래스를 지정한 ApplicationContext도 매번 새로 생성되는 상황이 발생합니다. 이를 방지하기 위해 @RunWith annotation은 각 테스트 별로 오브젝트가 생성되더라고 싱글톤의 ApplicationContext를 보장하는 역할을 합니다.

    @Test로 annotation이 붙은 method는 쓰레드 내에서 반복 실행됩니다. 이때 일반적인 JUnit Assertion을 사용하여 테스트 결과를 검증할 수 있습니다. Assertion에 실패할 경우, 해당 쓰레드에서 실행한 마지막 테스트가 실패 처리됩니다.

    @Test: @Test annotation이 선언되면 해당 method는 테스트 대상임을 뜻합니다.

    Assertion: 해당 문장이 실행 될 경우, 이 문장은 참이라고 단언할 수 있는 문장입니다. 즉 개발자가 개발한 프로그램에서 가정하고 있는 사실이 올바른 지 검사할 수 있도록 도와주는 기능입니다. 여기서 Assertion이 아니더라도 try/catch문이나 if/else문으로 에러 검사 기능을 처리할 수 있습니다. 여기서 차이점은 예외는 주로 프로그램을 실행하는 도중 예상하지 못한 상태를 처리하는데 사용합니다. 예를 들어, 파일을 열려고 하는데 열리지 않거나, 네트워크의 연결이 끊어지는 현상 등의 상태를 처리할 때 주로 사용됩니다. 반면 Assertion은 개발자가 참이라고 가정하는 상태를 명시하기 위해 사용됩니다. 예를 들어, 어떤 method가 파라미터로 양수만 입력 받아야 한다고 확신한다면, Assertion을 사용해 해당 사실(파라미터가 양수라는 것)을 명시할 수 있습니다.

    다시 말해, 예외는 프로그램의 코드가 실행되는 도중 발생하는 예외인 비정상적인 상태를 처리합니다. 하지만 Assertion은 프로그램이 올바르게 수행될 수 있는 조건을 명시해주어 해당 조건을 만족하는 경우에만 코드가 실행될 수 있도록 합니다. 

    nGrinder Groovy 테스트에서는 기존에 많이 사용되던 JUnit의 @BeforeClass, @Before, @AfterClass, @After 대신 다음과 같은 annotation을 사용합니다.



    설명적용사용 예

    @BeforeProcess

    프로세스가 생성될때 실행해야 하는 동작 정의

    static method 

    - 프로세스 내 쓰레드가 공유할 리소스 파일 로드

    - GTest를 사용한 테스트 항목Instrumentation

    @AfterProcess

    프로세스가 종료하기 직전에 실행해야 하는 동작 정의

    static method

    리소스파일 닫기

    @BeforeThread

    각 쓰레드가 실행된 전에 실행해야 하는 동작 정의

    member method

    - 테스트 대상 시스템 로그인.

    - 쓰레드 별 쿠키 핸들러 설정

    @AfterThread

    각 쓰레드가 종료하기 직전에 실행해야 하는 동작 정의

    member method

    - 테스트 대상 시스템 로그아웃

    @Before

    모든 @Test 메소드가 실행되기 전에 실행해야 하는 동작 정의

    member method

    - 여러 @Test 메소드가 공유하는 로직

    - 설정 검증

    @After

    모든 @Test 메소드가 종료된 이후 실행해야 하는 동작 정의

    member method

    - 거의 사용 안함

    @Test

    테스트 동작 정의

    member method

    Test body

    Groovy 스크립트 실행 흐름도



    어노테이션 별 분석

    @BeforeProcess

    	public static GTest test;
    	public static HTTPRequest request;
    	@BeforeProcess
        public static void beforeProcess() {
            // Instead of Test in Jython, GTest is used here
            // It's because the identifier "Test" is alredy used by the @Test 
            // GTest is the replacement to avoid the naming confliction.
            test = new GTest(1, "test1");
            request = new HTTPRequest();
            test.record(request);
            grinder.logger.info("before process.");
        }
    }

    모든 쓰레드가 공유할 데이터를 정의하기에 좋은 곳입니다. @BeforeProcess가 붙은 static method는 각 테스트 통계를 수집할 때 사용되는 GTest 인스턴스를 정의하고, request 인스턴스를 바이트 코드 조작(record method)를 통해 레코딩하도록 합니다. request 인스턴스에 대해 method 호출을 하게 되면 테스트 별로 TPS를 증가 시킵니다. 만약 다수의 HTTPRequest 객체를 레코딩해야 한다면 해당 객체를 test.record(request2)와 같이 처리하면 됩니다.

    @BeforeThread

        @BeforeThread
        public void beforeThread() {
            grinder.statistics.delayReports=true;
            grinder.logger.info("before thread.");
        }

    해당 함수는 쓰레드가 시작되기 전 실행되어야 할 부분을 적습니다. 보통 로그인 같은 테스트 사전 처리 코드를 넣습니다. 다음 실제 테스트를 작성합니다. 아래 @Test와 같이 @Test를 추가해 여러 test 함수를 정의할 수 있습니다.

    @Test

    	private boolean googleResult; 
    	@Test
    	public void testGoogle(){
        	googleResult = false;
        	HTTPResponse result = request.GET("http://www.google.com");
        	if (result.statusCode == 301 || result.statusCode == 302) {
            grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
        	} else {
            	assertThat(result.statusCode, is(200));
        	}
        	googleResult = true;
    	}
    	 
    	@Test
    	public void testYahoo(){
        	if (!googleResult) {
            	grinder.logger.warn("Just return. Because prev google test is failed.");
            	return;
        	}
        	HTTPResponse result = request.GET("http://www.yahoo.com");
        	if (result.statusCode == 301 || result.statusCode == 302) {
            	grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
        	} else {
            assertThat(result.statusCode, is(200));
        	}
    	}

    첫 번째 testGoogle() 테스트 함수를 실행한 다음, testYahoo()를 실행합니다. 그런데 googleResult라는 멤버 변수를 사용하여 testGoogle() 함수의 실행결과를 참조합니다. 이러한 방식은 JUnit에서는 각 테스트 별로 각각의 테스트 객체를 생성하기 때문에 불가능합니다. 그러나 GrinderRunner는 이와 같은 제약을 수정하여, 쓰레드 당 한개의 테스트 객체만을 사용합니다. 따라서 googleResult와 같은 멤버 변수 참조도 가능합니다.  

    댓글