Android – Robolectric를 활용한 Unit Testing

원문

Robolectirc 설정

RobolectricJUnit 4(단말기 필요)는 JVM환경에서 안드로이드 어플리케이션을 테스트 할 수 있는 프레임웍이다. 좀 더 많은 정보는 여기에서 얻을 수 있다.

테스트 환경

  • Android Studio 1.3.1 (1.2.1.1++)
  • Android Gradle Plugin 1.3.0 (1.2.3+)
  • Gradle 2.3.0 (2.2.1+)

Note. Robolectirc은 Android Studio 1.1에서 구동 될 수 있으나, Robolectric Gradle Plugin과 별도의 설정이 필요하다.

또한 Android Studio 1.1에서의 unit testing은 여기에서 확인 할 수 있다.

App build.gradle

첫번째, app 폴더 안에 build.gradle안에 아래의 내용을 추가해야 한다. 현재 최종 버전은 3.0.

dependencies {
    ...
    testCompile 'org.robolectric:robolectric:3.0'
  }

Android Studio 설정

다은 스탭으로.. Unit Testing을 하기 위해 몇가지 설정 할게 필요한데, 첫 번째로 왼쪽 사이드 메뉴에 있는 Build Variants눌러 준다. Test Artifact를 Unit Tests로 선택해준다.

각 타입 별 테스트를 위해, Android Studio는 아래와 같이 경로들을 사용한다. * Unit Tests => src/test/java * Android Instrumentation Tests => src/androidTest/java

기본으로 되어있는 Test는 Android Instrumentation Tests로 Unit Tests경로로 바꿔 준다. 가장 쉬운 방법으로 rename 이름 변경 기능을 사용하자.

Note. 맥을 사용한다면, 경로 이슈가 있는데 아래와 같이 변경해주면 문제를 해결 할 수 있다.

Go to Run -> Edit Configurations -> Default -> Junit으로 이동하면 working directory를 [code]$MODULE_DIR$[/code]로 변경해준다.

좀 더 많은 자료는 여기에서 확인 할 수 있다.

모든 설정이 완료 되었다.

Robolectric test 코드 작성

  • Project Name : RSample
  • MyApplication
public class MyApplication extends Application {
    public LoginHandler getLoginHandler() {
        return new LoginHandler();
    }
}
  • LoginHandler
public class LoginHandler {
    public void login(String id, String pwd) {
    }
}
  • MainActivityTest
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;

import static java.lang.System.out;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.robolectric.shadows.ShadowLog.stream;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class MainActivityTest {
    MainActivity activity;
    private LoginHandler loginHandler;
    private MyApplication mockedApplication;

    @Before
    public void setup() throws Exception {
        stream = out;

        activity = Robolectric.setupActivity(MainActivity.class);
        activity = Mockito.spy(activity);

        mockedApplication = mock(MyApplication.class);
        loginHandler = mock(LoginHandler.class);
        when(mockedApplication.getLoginHandler()).thenReturn(loginHandler);
        doReturn(mockedApplication).when(activity).getApplication();
    }

    @Test
    public void validateTextViewContent() throws Exception {
        TextView tvHelloWorld = (TextView) activity.findViewById(R.id.a_main_titleTv);
        assertThat(tvHelloWorld.getText().toString(), is("Hello world!"));
    }

    @Test
    public void login() throws Exception {
        Button loginBtn = (Button) activity.findViewById(R.id.a_main_loginBtn);
        loginBtn.performClick();
    }
}
  • MainActivity
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final EditText idEt = (EditText) findViewById(R.id.a_main_idEt);
        final EditText pwdEt = (EditText) findViewById(R.id.a_main_pwdEt);
        Button loginBtn = (Button) findViewById(R.id.a_main_loginBtn);

        loginBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String id = idEt.getText().toString();
                String pwd = pwdEt.getText().toString();
                login(id, pwd);
            }
        });
    }

    private void login(String id, String pwd) {
        getLoginHander().login(id, pwd);
    }

    private LoginHandler getLoginHander() {
        MyApplication app = (MyApplication) getApplication();
        return app.getLoginHandler();
    }
}
  • activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:paddingLeft="@dimen/activity_horizontal_margin"
              android:paddingRight="@dimen/activity_horizontal_margin"
              android:paddingTop="@dimen/activity_vertical_margin"
              android:paddingBottom="@dimen/activity_vertical_margin"
              android:orientation="vertical"
              tools:context=".MainActivity">

    <EditText
        android:id="@+id/a_main_idEt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Your id"/>

    <EditText
        android:id="@+id/a_main_pwdEt"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:password="true"
        android:hint="Your pwd"/>

    <TextView
        android:id="@+id/a_main_titleTv"
        android:text="@string/hello_world"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

    <Button
        android:id="@+id/a_main_loginBtn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Login"/>
</LinearLayout>

이제 Run MainActivityTest(Shift + F10)를 통해 테스트가 가능하다.

끝.

참조1
참조2

Emacs > 맥을 위한 이맥스 Daemon 설정하기

(참조)[https://gist.github.com/ambethia/304964]

여러가지 스크립트가 포함되어 있는 이맥스를 실핼할 때 반복적인 시간이 소요 되는데 이를 해소 할 수 있는 방법을 소개합니다.

맥에 이맥스가 이미 설치 되어 있다는 가정하에 문서를 작성하였습니다.

이맥스 데몬 어플리케이션 생성하기

맥에서 ‘Script Editor’를 열어 아래의 내용을 입력해주세요.

tell application "Terminal"
do shell script "/Applications/Emacs.app/Contents/MacOS/Emacs --daemon"
endtell

저장 할 때는 File > Export, 그리고 File Format : Application을 선택하여 Applications/ 폴더에 저장해주세요.

이상 이맥스 데몬 어플리케이션을 생성하였습니다.

이제 맥이 실행될 때 이 앱을 실행 시켜주기 위해서 어플리케이션을 등록해 봅시다.

  • System Preferences > Accounts 열기
  • 계정 선택 후 Login Items탭 선택
  • ‘+’버튼을 눌러 Emacs Daemon을 선택
  • hide 체크방식에 대해서는 생각할 필요 없음. (나중에 앱 아이콘을 없앨 예정)

이맥스 클라이언트 어플리케이션 생성하기

다시 한번 Script Editor를 열어 아래의 내용을 넣어주세요.

tell application "Terminal"
do shell script "/Applications/Emacs.app/Contents/MacOS/bin/emacsclient -c"
endtell

저장 할 때는 위와 동일하게 Application타입을 선택해주시고, Emacs Client 이름으로 Applications/폴더에 저장해주세요.

새로 생성한 Emacs Client에 아이콘 적용하기

  • Finder를 열어 Application Folder 선택해주세요.
  • Emacs application선택 후 우측 버튼 > Show Package Contents를 선택해주세요.
  • Contents/Resources 폴더 안에 Emacs.icns를 Emacs Client 폴더안에 있는 Content/resources 폴더에 넣어주세요. 그리고 기존에 있던 applet.icns를 삭제하시고 Emacs.icns파일을 applet.icns파일 이름으로 변경해주세요.

Emacs Client가 동작 되는 동안 Dock에서 Emacs 아이콘을 숨겨보자.

  • Application folder에 있는 Emacs Application (Emacs Client가 아님), Show Package Contents로 열어주세요.
  • Info.plist파일을 열어 Add child를 통해 키 NSUIElement, 값 1을 설정해주세요.

모든 설정이 완료 되었네요.

그럼 즐거운 이맥스 라이프!!

Android – Kakaolink 활용하기

kakaolink를 활용하여 특정 문자열 서드파트 어플리케이션에 전달하고자 한다.

상황 예)
전송자 : 카톡을 활용하여 친구에게 메시지를 보낸다. “사전에서 ‘apple’이라는 걸 검색해봐~”
수신자 : 카톡에서 받은 메시지와 함께 사전 검색 버튼이 생성된다. 버튼을 누르게 되면 사전앱이 실행되고 자동으로 검색되게 된다.

kakalink

위와 같은 상황에서 kakaolink를 적극적으로 활용할 수 있다.

  • 어플리케이션 호출 및 문자열 데이터를 받기 위한 권한 부여

    strings.xml 파일

    // key 선언
    <string name="kakao_app_key">000000000000000000</string>
    <string name="kakao_scheme">kakao000000000000000000</string>
    <string name="kakaolink_host">kakaolink</string>

AndroidManifest.xml

         <activity
            android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
                <category android:name="android.intent.category.BROWSABLE" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="@string/kakao_scheme" android:host="@string/kakaolink_host" />
                <data android:scheme="kakao000000000000000000" />
            </intent-filter>
        </activity>
  • Share Mehtod 작성
        try {
            final KakaoLink kakaoLink = KakaoLink.getKakaoLink(a);
            final KakaoTalkLinkMessageBuilder kakaoTalkLinkMessageBuilder = kakaoLink.createKakaoTalkLinkMessageBuilder();
            AppActionInfo aai = new AppActionInfo(AppActionInfo.ACTION_INFO_OS.ANDROID, AppActionBuilder.DEVICE_TYPE.PHONE, "params=test", "referrer=kakaotalklink");
            AppActionBuilder aab = new AppActionBuilder().addActionInfo(aai);
            kakaoTalkLinkMessageBuilder.addAppButton("Open the app", aab.build());
            final String linkContents = kakaoTalkLinkMessageBuilder.build();
            kakaoLink.sendMessage(linkContents, a);
        } catch (KakaoParameterException e) {
            e.printStackTrace();
        }
  • 문자열 데이터를 받는 Activity
         Uri uri = getIntent().getData();
         if (uri != null){
              String param = uri.getQueryParameter("params");
              // do something              
         }

Android Kakaolink Ref
Android kakaolink guide

Clojure – ` syntax-quote와 ‘ quote의 차이점에 대해서

`syntax-quote와 'quote의 차이점에 대해서 알아보자.

첫번 째 syntax-quote는 namespace를 반환한다.
user>'x
x

user>`x
user/x

두번 째 syntax-quote는 ~unquote 또는 ~@unquote splicing 매크로를 사용할 수 있다.

user> `(this ~(symbol (str "i" "s" - "cool")))
(user/this is-cool)

user> '(this ~(symbol (str "i" "s" - "cool")))
(this (clojure.core/unquote (symbol (str "i" "s" - "cool"))))

위와 같이 두가지 차이점을 알수 있었다. 더 자세한 내용은 아래의 링크를 통해서 확인 할 수 있다.

reference

Clojure – ` syntax quote에 대해서

`syntax quote에 대해서 알아보자.

`와 연관있는 ~ unquote, 그리고 ~@unquote splicing이 있다.

위 두가지는 syntax quote를 이용하였을 때 그 반대의 영향을 주기 위해서 사용된다.

심볼을 심볼 그 자체로 활용하기 위해 사용된다. 'quote와 비슷하면서도 다르다. sytax quote 설명은 다른 장에서 알아보자.


user> (def five 5)
user/five
user>`five
user/five

p>한가지 기억하자. Clojure에서 모든 것들이 데이터이다. 위와 같이 quote를 사용하였을 때 평가되지 않고 심볼 자체 또는 리스트 자체로 출력이 되는 것을 볼수 있다.

그럼 왜 quote가 필요할까? 한가지 더 실험을 해보자.

(1 2 3)
Unhandled java.lang.ClassCastException
java.lang.Long cannot be cast to clojure.lang.IFn

` 위와 같이 에러가 발생된다. 이유는 현재 1이라는 심볼이 정의 되어 있지 않기 때문에 발생된다. (, )에 둘러 쌓여있을 때 첫번 째 argument는 심볼로 인식된다.

다시 quote를 활용해보자.


user>`(1 2 3)
(1 2 3)

에러 없이 정상으로 작동된다.

다음 장에서 `syntax-quote와 '의 차이점에 대해서 알아보자.

reference

Clojure – ~@ unquote splicing macro에 대해서

~@ unquote splicing macro에 대해서
unquoute splicing macro에 대해서 이해하기 위해서는 먼저 unquote macro에 대해서 알아야한다.

unquote splicing macro는 ~@로 기록하도록 하겠다.

~@는 form에 있는 심볼들을 모두 평가하기 위해 사용된다.

예제를 통해서 이해해보도록 하자.


user> (def three-and-four (list 3 4))

'user/three-and-four

위와 같이 심볼을 하나 만들었다.

그리고 그저 ~unqoute 로만 평가를 했을 때

user> `(1 ~three-and-four)
(1 (3 4))

위와 같이 평가된다.

여기서 unqoute splicing macro를 이용하여 폼 안에 있는 모든 심볼을 평가하고 싶을 때 아래와 같이 사용할 수 있다.


user> `(1 ~@three-and-four)
(1 3 4)

이상으로 unqoute macro와 unquote splicing macro에 대해서 알아보았다.

Clojure – ~ (unqoute)에 대해서

~ unqoute 매크로. ~ 는 unqoute 라고 읽는다. unqoute를 이해하기 위해서는 ` (syntax quote)에 대해서 알아야한다. 그럼 일단 syntax sqoute에 대해서 알아보자. 영어에서 syntax는 문법, 구문론 등으로 해석되며, qoute는 `으로 “인용하다.” 또는 “전달하다.”로 해석된다.

그럼 Clojure에서는 어떻게 사용될까?

클로저에서 메서드에 syntax qoute를 사용했을 때 말 그대로 메서드 자체(구문론 자체를)를 그대로 전달하라는 의미를 갖는다. 아래와 같다.


user> `(+ 1 1) (clojure.core/+ 1 1)

위와 같이 심볼 자체로 출력된다. unqoute는 이때 사용된다. 심볼을 평가하고 싶을때 아래와 같이 사용된다.


user>`~(+ 1 1) 2

심볼을 평가하고 싶을때 unqoute를 사용하면 된다.

위와 같이만 예로 들 경우 어디에서 사용해야 하는지 감이 제대로 잡히지 않는다. 좀 더 깊히 들어가보자.


user> (def three-and-four (list 3 4))

\'user/three-and-four

user> `(1 three-and-four)

어떤 값이 나올까? 예상하는 바와 같이 아래와 같이 출력된다. \'user/three-and-four

평가되지 않았다. 위 unqoute때문에 평가되지 않았다. 이때 1은 data이므로 평가하지 않아야하고 three-and-four만 평가를 하고 싶을때 아래와 같이 사용할 수 있다.

user>`(1 ~three-and-four)

어떻게 될까? (1 (2 3))


위와 같이 평가되어 위와 같이 출력된다.


다음장에서 ~@ unquote splicing macro를 사용하여 다른값을 유추해보자.

Android – Gradle을 이용하여 Android Library 만들기

Gradle을 이용하여 Android Library 만들기

Android Studio에서 새로운 프로젝트를 만들어준다. 이것은 실제 라이브러리 프로젝트가 아니라, TDD를 위한 껍대기라고 생각하면 된다.

이제 라이브러리 프로젝트를 만들어보자. New > Module로 새로운 라이브 프로젝트를 만들자.

Build 하기

$ gradle <lib>:build

만약 라이브러리 이름이 util이라면 아래와 같은 Command를 입력해준다.

$ gradle util:build

아무 이상없이 잘 빌드 되었다면 아래와 같은 결과를 확인 할 수 있다.

... 
:util:preCompileDebugUnitTestJava
:util:compileDebugUnitTestJava UP-TO-DATE
:util:compileDebugUnitTestSources UP-TO-DATE
:util:mockableAndroidJar
:util:assembleDebugUnitTest
:util:testDebug
:util:preCompileReleaseUnitTestJava
:util:compileReleaseUnitTestJava UP-TO-DATE
:util:compileReleaseUnitTestSources UP-TO-DATE
:util:assembleReleaseUnitTest
:util:testRelease
:util:test
:util:check
:util:build

BUILD SUCCESSFUL
$ cd util/build/outputs/aar/

위 폴더에서 util-debug.arr, util-release.arr를 확인 할 수 있다.

Depoloy 시키기

안드로이드 프로젝트의 /libs 폴더 안에 추가하자.
그리고 build.gradle파일을 아래와 같이 추가하자.

dependencies {
    compile 'package.name.of.your.aar:util@aar'
}

repositories{
    flatDir{
        dirs 'libs'
    }
}

추가가 정확히 되었다면 Tools > Android > Sync를 누르면 모든 작업이 완료된다.

AAR Format에 대해서

‘aar’ 번들은 Android Library Project에서 사용되는 배포 바이너리 파일이다.

파일 확장자는 항상 .arr로 끝나며, maven artifact 타입으로 잘 알려져있다. 사실 이 파일은 zip파일이며 아래와 같은 구조를 가진다.

  • /AndroidManifest.xml (mandatory)
  • /classes.jar (mandatory)
  • /res/ (mandatory)
  • /R.txt (mandatory)
  • /assets/ (optional)
  • /libs/*.jar (optional)
  • /jni//*.so (optional)
  • /proguard.txt (optional)
  • /lint.jar (optional)

참고

Tool – wireshark 설치

brew를 이용한 whireshark 설치 방법에 대해서 기술한다.


1) 기존 wireshark 삭제하기

$ sudo bash -c "$(curl -sL https://raw.github.com/x0b4dc0d3/wireshark-uninstall-osx/master/uninstall.sh)"

2) brew를 이용하여 설치하기

$ brew install wireshark

만약 GUI버전이 필요하다면 QT와 함께 설치 해주어야 한다.

$ brew reinstall wireshark --with-qt