Unit Testing ThingWorx Extensions Using the Extension SDK in Eclipse
Thingworx 쪽은 유의미한 한글 검색 결과가 많지가 않다.
그래서 시리즈 제목들을 전부 영어로 해놨는데 검색 유입이 과연 될지 조금 걱정이 된다..(이제와서?)
H1 헤더 남발하면 검색 노출 잘 안될 수도 있다는 글을 봤고, velog는 내용에 H1 헤더를 적극적으로 써서인지 검색이 잘 안됐었는데 Quartz4에서는 어떨지 모르겠다.
다음에 기회가 되면 google 검색 노출 관련으로 분석을 해봐야겠다.
삽질 배경
Thingworx Extension을 개발하다 테스트에 대한 편의 향상에 대한 필요성을 느꼈다.
(조금이라도 더 편한 방법에 집착하는편..🤔)
과거에 Widget Extension에 대한 작업을 할 때는, 위젯 특성 상 무조건 Mashup에서 추가해야 UI확인, 상호작용 확인, 데이터 확인이 가능하기 때문에 무식하게 로그만 잔뜩 찍게 세팅해두고 Extension 빌드 > Thingworx에 Import > 화면에서 테스트를 무한 반복하는 방식을 사용했었다.
하지만 Thing Template에서 Service 개발 같은 경우에는 그냥 스크립트인데 매번 빌드해서 Import 하고 필요하면 서버 재시작까지 해서 테스트 해 보는 것은 테스트 한사이클 시간도 오래걸리고 너무나도 비효율적인 것이다.
그래서 그냥 JUnit 세팅해서 테스트 해보려 했는데 테스트 데이터 세팅에서 실패했다. 모든 Entity들이 객체 생성도 되고 메서드 호출도 되길래 데이터 세팅도 해줬는데 로그 찍어보니 Null이 나왔던 것이다.
사실 나와 비슷한 의문, 불편함, 고충이 있는 사람들이 많을 것 같아서 PTC 공식 가이드나 아티클이 분명 있을거라고 생각했는데 PTC Support를 아무리 뒤져도, PTC Community를 아무리 뒤져도 명쾌한 해답을 못찾았다.(다들 빌드하고 Import하고 재시작해서 테스트 하는 노가다 방식에 순응한 것일까..)
그래서 준비했다.
삽질 과정과 알아낸 단위 테스트 방법 대공개~
실패에 대한 내용은 간단히 현상, 원인, 결과만 정리했고, 이후 적용해야하는 자세한 방법은 마지막 목차에 성공 방법을 한번에 정리해 두었으니 그쪽을 참고 바란다.
문제 해결 과정
첫번째 실패(Logger 문제)
단위 테스트를 해보자 라고 결심하자마자 일단 Thingworx Extension 프로젝트에 무작정 JUnit5 설정을 시도했다.
현상
테스트 코드 작성 후 단위 테스트 실행을 했는데 아래 에러 로그를 내면서 실패했다.
java.lang.ExceptionInInitializerError
at net.innfoactory.tw.TestThingTest.testTestServiceReturnInfoTable(TestThingTest.java:49)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.lang.NullPointerException:
Cannot invoke "com.thingworx.logging.LogUtilities.getApplicationLogger(java.lang.Class)"
because the return value of "com.thingworx.logging.LogUtilities.getInstance()" is null
at net.innfoactory.tw.TestThing.<clinit>(TestThing.java:15)
... 4 more원인
원인은 Logger 문제로 다행히 에러로그에 명확히 나와서 해결이 쉬웠다.
TestThing이라는 이름의 Thing Template 생성 후 Service를 추가하면 아래와 같이 자동으로 Logger 선언이 추가된다.
private static Logger _logger = LogUtilities.getInstance().getApplicationLogger(TestThing.class);자동 추가된 Logger 선언은 Thingworx 컨택스트 내에서 초기화 되는 것을 가정하고 있기 때문에 Thingworx 플랫폼 외부 환경에서 클래스가 로딩될 때 LogUtilities.getInstance()가 null이 되어 NullPointerException이 발생했던 것이다.
해결
예외처리를 추가하여 해결하였다.
Thingworx 환경인 경우에는 자동으로 추가된 선언처럼 LogUtilities.getInstance()를 사용해서 Logger 선언을 하면되고,
Thingworx 환경이 아닌 경우에는 LoggerFactory를 사용해서 Logger 선언을 한다.
자세한 수정 방법은 마지막 목차에 정리해 두었으니 단위 테스트 세팅 가이드를 참조하면 된다.
두번째 실패(더미 데이터 생성 문제)
테스트를 하려면 Input parameter로 줄 데이터가 있어야 한다.
REST로 TW 라이브 데이터를 조회해서 쓰거나, 테스트 내에서 더미 데이터를 만들어 주는 방법이 있을 것이다.
물론 REST로 TW 라이브 데이터를 조회 하는 방법을 사용하면 실제 시스템과 더 유사한 조건으로 테스트를 수행할 수 있겠지만 AppKey 설정과 HTTP 통신을 위한 갖은 설정을 해야한다.
나는 AppKey를 사용하고 싶지 않았고(경험상 AppKey는 어떤 외부 시스템에서 사용중인지 파악하기 어렵기 때문에 무분별하게 쓰면 관리가 잘 안되서 사용을 최소화 하고싶음) 어차피 테스트가 끝나고 Extension 빌드할 때는 갖은 설정들을 롤백해주어야 하기 때문에 테스트 내에서 더미 데이터를 선언하고자 했다.
Thingworx에서 사용하는 Primitive 타입들이 있는데 서비스에 파라미터를 건내줄 때에는 이 Primitive 타입으로 값을 구성해주어야 한다.
이 중 가장 까다로운 InfoTable을 구성해보고자 했다.
현상
Thingworx Composer에서 작업할 때도 서비스 스크립트 내에서 InfoTable 구성이 필요한 경우가 있다.
InfoTableFunctions 라는 Resource의 서비스를 사용해서 구성할 수 있고 Snippet도 제공되는데, Composer의 서비스 스크립트에서 구성할 때의 순서는 아래와 같다.
name(STRING), email(STRING) 속성으로 구성되어있는 InfoTable을 선언해보겠다.
- DataShape로부터 Infotable을 생성한다.(TestDataShape 라는 DataShape이 이미 생성되어있어야함)
let testInfoTable = Resources["InfoTableFunctions"].CreateInfoTableFromDataShape({
infoTableName: "testInfoTable",
dataShapeName: "TestDataShape"
});- InfoTable에 추가할 Row를 구성한다.
let newEntry = {
name: "Lee",// STRING
email: "lee@test.com"// STRING
};- InfoTable에
AddRowfunction을 통해 Row를 추가한다.
testInfoTable.AddRow(newEntry);테스트 코드에서 InfoTable을 구성하는 것은 Java로 하는거라 Primitive 타입 등 신경써줘야 하는 것들이 조금 있지만 절차는 동일하다.
Extension 개발용 SDK에서 API는 제공을 해주고 있으니 아래와 같이 처리하면 된다.
- DataShape를 선언한다.
// #. Create DataShape
DataShapeDefinition dsd = new DataShapeDefinition();
// #. Add fields: name
dsd.addFieldDefinition(new FieldDefinition("name", BaseTypes.STRING));
// #. Add fields: email
dsd.addFieldDefinition(new FieldDefinition("email", BaseTypes.STRING));- DataShape로 InfoTable를 선언한다.
// #. Create Empty Infotable
InfoTable testInfo = new InfoTable(dsd);- InfoTable에 추가할 Row를 구성한다.
// #. Add Row 1
ValueCollection row1 = new ValueCollection();
row1.SetStringValue("name", new StringPrimitive("Kim"));
row1.SetStringValue("email", new StringPrimitive("kim@test.com"));
// #. Add Row 2
ValueCollection row2 = new ValueCollection();
row2.SetStringValue("name", new StringPrimitive("Lee"));
row2.SetStringValue("email", new StringPrimitive("lee@test.com"));- InfoTable에
AddRowfunction을 통해 Row를 추가한다.
testInfo.addRow(row1);
testInfo.addRow(row2);이렇게 생성한 testInfo 라는 InfoTable을 서비스에 넘겨주어 테스트 케이스를 돌려봤다.
내가 만든 서비스는 Input으로 InfoTable을 받으면, InfoTable의 Row 개수를 출력해주는 서비스이다.
위에서는 분명 Row를 2개 만들어서 InfoTable을 만들었으니, 2가 출력되었어야 했는데 null이 나오는 문제가 발생했다.
모든 코드 사이사이에 객체가 제대로 생성되고 값은 제대로 설정되었는지 로그를 찍어보았는데 왠걸, 객체생성은 되는데 값이 모두 null이다.
모든 객체가 빈 껍데기만 생성되고 있었던 것이다.
원인
결론부터 이야기 하자면, ThingWorx Extension SDK는 ThingWorx 플랫폼 위에서 동작할 전제 하에 동작 stub만 제공하기 때문에 실질적인 내부 구현체가 없었던 것이다.
PTC Community에서 유사 케이스를 찾다가 다른 유저들도 나와 동일하게 InfoTable을 분명 구성했는데도 null이 나온다는 사례를 보았고 그 답변중에 “The extensionSDK just has Dummy implementations” 라는 코멘트가 있어서 힌트가 되었다.
내 Eclipse에는 Thingworx OOTB 뜯어보려고 Enhanced Class Decompiler를 세팅해놨기 때문에 구현체 확인을 해 보았다. 아니나 다를까 DataShapeDefinition의 addFieldDefinition도, InfoTable의 addRow도, ValueCollection의 SetStringValue도 다 텅 비어있었다.
해결
그럼 구현체를 구할 방법은 없는것인가?
사실 무식하게 냅다 Thingworx OOTB 라이브러리를 추가해봤는데 의존성이랑 컨텍스트 문제랑 이것저것 걸려서 실패했다.
그래서 서치를 해보니 Thingworx Edge SDK에는 구현체가 있다는 정보를 확인했다.
Thingworx Edge SDK는 ThingWorx 플랫폼과 통신하는 머신(로봇)/기기용 애플리케이션을 개발하기 위한 SDK이다. Thingworx에서 로봇이 RemoteThing으로서 통신하려면, 그리고 데이터를 주고 받고 속성이나 서비스에 접근해서 사용하려면 구현체가 분명히 있어야 할 것이다.
Thingworx Edge SDK의 구현체가 있는 라이브러리인 thingworx-java-sdk-{버전}.jar만 추가해서 사용하면 될 것이라 생각했는데 막상 추가하니 IDE 상에서 테스트 코드에 로드되는 구현체는 thingworx-ext-sdk-{버전}.jar에 포함된 구현체였다.
이 문제는 단순히 클래스 충돌문제라 빌드패스에서 우선순위만 변경하여 해결을 할 수 있었다.
그리고 예상대로 구현체가 있는 JAR를 로딩하니 InfoTable의 값이 null이 아니고 의도한 대로 구성되는 것을 확인했다.
이 작업도 마지막 목차에 정리해 두었다.
단위 테스트 세팅 가이드(최종 해결방법)
Thingworx Extension 개발은 Eclipse 플러그인으로 지원되니까 Eclipse 환경 기준으로 설명한다.
정리하면 JUnit 세팅, Logger 수정, ThingWorx Edge SDK추가하면 Eclipse 상에서도 서비스를 테스트 할 수 있다.
단위 테스트를 위한 JUnit 세팅
-
프로젝트 우클릭해서 Java Build Path 설정에서 Libraries 탭 오픈

-
Add Library를 통해 JUnit 선택

-
JUnit5 추가

-
테스트 할 서비스가 포함된 객체 우클릭해서 “New > Other..” 선택 후 JUnit Test Case 생성

Logger 수정
private static Logger _logger = LogUtilities.getInstance().getApplicationLogger(TestThing.class);서비스 추가 시 자동으로 생성되는 위의 코드를 아래의 코드로 변경하여 예외처리한다.
private static final Logger _logger;
static {
Logger tempLogger = null;
try {
if (LogUtilities.getInstance() != null) {
tempLogger = LogUtilities.getInstance().getApplicationLogger(TestThing.class);
}
} catch (Exception e) {
// 테스트 환경 등 플랫폼 미기동 상태
}
if (tempLogger == null) {
// ThingWorx 외부 환경용 로거
tempLogger = LoggerFactory.getLogger(TestThing.class);
}
_logger = tempLogger;
}ThingWorx Edge SDK 설정
Thingworx Edge SDK를 다운받아서 BuildPath에 추가 후 우선순위를 조정해준다.
- PTC Support에서 Thingworx Edge SDK 다운로드 후 압축해제하여 “/thingworx-java-sdk/lib” 경로에 있는
thingworx-java-sdk-{버전}.jar를 준비 - 프로젝트 우클릭해서 Java Build Path 설정에서 Libraries 탭 오픈
- Add External Jar를 통해
thingworx-java-sdk-{버전}.jar추가

- Java Build Path 설정의 Order and Export 탭 오픈
thingworx-ext-sdk-{버전}.jar보다thingworx-java-sdk-{버전}.jar의 우선순위가 높도록 조정

⚠️ 동일 클래스명이 존재하더라도, 우선순위 상 위에 있는 JAR가 먼저 로딩됩니다.
위의 과정으로 테스트 코드의 Entity 생성에 thingworx-edge-extension.jar 라이브러리가 사용된다.
단위 테스트 진행
테스트 코드 작성 후 “Run As > JUnit Test” 선택하여 단위 테스트 실행한다.
빌드 전 테스트 설정 롤백과 테스트 코드 제거 수행
위의 모든 설정을 완료하면 JUnit을 통해 서비스 테스팅은 가능하나, ANT를 통한 Extension 빌드를 시도하면 오류가 난다.
모든 테스트가 완료되고 빌드하고자 할 때에는 추가한 Edge sdk 라이브러리를 제거하고 생성했던 테스트 케이스를 삭제한 뒤 빌드를 수행해주자.
마무리
ANT 빌드 스크립트가 Eclipse Thingworx Plugin 쪽에서 ThingWorx Extension Project를 생성하면 자동으로 생성되는 파일이라 수정하지 않았지만, 사실 빌드 스크립트 쪽을 건들였으면 테스트 설정들을 롤백하거나 테스트 코드를 다시 제거하는 수고로움이 없는 우아한 방법을 찾을 수 있었을지도 모르겠다.
하지만 DevOps 환경과 같은, 다른 환경에서도 빌드 자동화 할 수 있는 가능성을 열어놓고 저울질 했을때 빌드 스크립트는 건들이지 않고 모든 Extension 프로젝트들에 일률적인 빌드 스크립트를 유지하는편이 안전할 것 같아 시도하지 않았다.
안정성과 편리함, 우아함의 황금률을 찾는 과정은 대부분 정답은 없기 때문에 고려할게 너무 많아 골치아프면서도 흥미로운 것 같다.