프레임워크 미니멀리즘과 소프트웨어 아키텍처를 통해 테스트가 쉬워졌습니다.
게시 됨: 2022-03-10다른 많은 Android 개발자와 마찬가지로 플랫폼에서 처음 테스트를 시작하면서 사기를 꺾는 수준의 전문 용어에 즉시 직면하게 되었습니다. 게다가, 내가 그 당시(2015년경) 발견한 몇 가지 예는 TextView.setText ( …) 제대로 작동하고 있었고 합리적인 투자였습니다.
설상가상으로 나는 이론이나 실제에서 소프트웨어 아키텍처에 대한 실질적인 이해가 없었습니다. 즉, 이러한 프레임워크를 배우기가 귀찮더라도 몇 개의 god
클래스로 구성된 모놀리식 애플리케이션에 대한 테스트를 작성했을 것입니다. 스파게티 코드 에서. 핵심은 이러한 응용 프로그램을 구축, 테스트 및 유지 관리하는 것은 프레임워크 전문 지식과 상관없이 자기 파괴의 연습이라는 것입니다. 그러나 이러한 깨달음은 모듈식 의 느슨하게 결합된 응집력이 높은 애플리케이션을 구축한 후에야 분명해집니다.
여기에서 우리는 이 기사의 주요 논의 요점 중 하나에 도달합니다. 여기에서 일반 언어로 요약할 것입니다. 소프트웨어 아키텍처의 황금 원칙 적용의 주요 이점 중(걱정하지 마십시오. 간단한 예제와 언어), 코드를 테스트하기가 더 쉬워질 수 있다는 것입니다. 이러한 원칙을 적용하면 다른 이점이 있지만 소프트웨어 아키텍처와 테스트 간의 관계가 이 기사의 초점입니다.
그러나 우리가 코드를 테스트하는 이유와 방법을 이해하고자 하는 사람들을 위해 먼저 유추에 의한 테스트의 개념을 탐색할 것입니다. 전문 용어를 외울 필요 없이. 주요 주제에 대해 더 깊이 들어가기 전에 테스트 프레임워크가 왜 그렇게 많은지에 대한 질문도 살펴보겠습니다. 이를 검사하면 이점, 제한 사항 및 대안 솔루션까지 볼 수 있기 때문입니다.
테스트: 왜 그리고 어떻게
이 섹션은 노련한 테스터에게 새로운 정보가 아니지만 그럼에도 불구하고 이 비유를 즐길 수 있습니다. 물론 저는 로켓 엔지니어가 아니라 소프트웨어 엔지니어입니다. 그러나 잠시 동안 물리적 공간과 컴퓨터의 메모리 공간 모두에서 객체를 설계하고 구축하는 것과 관련된 비유를 빌리겠습니다. 매체가 바뀌는 동안 프로세스는 원칙적으로 매우 동일하다는 것이 밝혀졌습니다.
잠시 우리가 로켓 엔지니어이고 우리의 임무가 우주 왕복선의 첫 번째 단계* 로켓 부스터를 만드는 것이라고 가정해 보겠습니다. 또한 다양한 조건에서 구축 및 테스트를 시작하기 위한 첫 번째 단계를 위한 서비스 가능한 설계를 생각해 냈다고 가정합니다.
첫 번째 단계는 로켓이 처음 발사될 때 발사되는 부스터를 말합니다.
프로세스를 시작하기 전에 내가 이 비유를 선호하는 이유를 지적하고 싶습니다. 인간의 생명이 위태로운 상황에 놓기 전에 우리가 왜 우리 디자인을 테스트하려고 애쓰는지에 대한 질문에 대답하는 데 어려움이 없어야 합니다. 출시 전에 애플리케이션을 테스트하면 생명을 구할 수 있다고 확신하지는 않겠지만(응용 프로그램의 특성에 따라 가능할 수도 있음) 평점, 리뷰 및 작업을 절약할 수 있습니다. 가장 넓은 의미에서 테스트는 단일 부품, 여러 구성 요소 및 전체 시스템이 실패하지 않는 것이 매우 중요한 상황에서 사용하기 전에 작동하는지 확인하는 방법입니다.
이 비유의 방법 측면으로 돌아가서 엔지니어가 특정 설계를 테스트하는 프로세스인 중복성 을 소개하겠습니다. 중복성은 원칙적으로 간단합니다. 테스트할 구성 요소의 복사본을 출시 시 사용하려는 것과 동일한 설계 사양으로 빌드합니다. 전제 조건 및 변수를 엄격하게 제어하는 격리된 환경에서 이러한 복사본을 테스트합니다. 이것이 로켓 부스터 가 전체 셔틀에 통합되었을 때 제대로 작동한다는 보장은 없지만 통제된 환경에서 작동하지 않으면 전혀 작동하지 않을 가능성이 매우 낮습니다.
로켓 설계 사본 이 테스트된 수백 또는 수천 개의 변수 중 로켓 부스터 가 테스트 발사될 주변 온도에 영향을 미친다고 가정합니다. 섭씨 35°에서 테스트하면 모든 것이 오류 없이 작동한다는 것을 알 수 있습니다. 다시 말하지만, 로켓은 실패 없이 대략 실온에서 테스트됩니다. 최종 테스트는 발사 지점에 대해 기록된 최저 온도인 -5°C에서 이루어집니다. 이 마지막 테스트 동안 로켓이 발사되지만 잠시 후 로켓이 타오르고 얼마 지나지 않아 격렬하게 폭발합니다. 그러나 다행히 통제되고 안전한 환경에서.
이 시점에서 우리는 온도 변화가 실패한 테스트와 관련이 있는 것으로 보이며, 이는 로켓 부스터 의 어떤 부분이 추운 온도에 의해 부정적인 영향을 받을 수 있는지 고려하게 합니다. 시간이 지남에 따라 한 구획에서 다른 구획으로 연료의 흐름을 막는 역할을 하는 고무 O-링인 한 가지 핵심 구성 요소가 빙점에 근접하거나 그 이하의 온도에 노출되면 단단해지고 효과가 없다는 사실이 밝혀졌습니다.
그의 비유가 챌린저 우주왕복선 참사의 비극적 사건에 느슨하게 기초하고 있음을 알아차렸을 가능성이 있습니다. 익숙하지 않은 사람들에게 슬픈 진실(조사가 결론을 내리는 한)은 실패한 테스트와 엔지니어의 경고가 많았지만 관리 및 정치적 우려로 인해 출시가 계속 진행되도록 박차를 가했습니다. 어쨌든 중복성 이라는 용어를 암기했는지 여부에 관계없이 모든 종류의 시스템 부분을 테스트하기 위한 기본 프로세스를 이해했기를 바랍니다.
소프트웨어에 관하여
이전 비유가 로켓 테스트의 기본 프로세스를 설명했지만(세부 사항은 최대한 자유롭게 사용하면서) 이제 귀하와 나에게 더 관련이 있는 방식으로 요약하겠습니다. 어떤 종류의 배포 가능한 상태가 되면 장치에 적용할 수 있습니다. 대신 먼저 응용 프로그램의 개별 부분에 중복성 의 원칙을 적용할 수 있다고 가정합니다.
이것은 우리가 전체 애플리케이션의 더 작은 부분(일반적으로 소프트웨어 단위 라고 함)의 복사본을 만들고, 격리된 테스트 환경을 설정하고, 발생할 수 있는 모든 변수, 인수, 이벤트 및 응답을 기반으로 작동하는 방식을 확인한다는 것을 의미합니다. 런타임에. 테스팅은 이론상으로 간단하지만 이 프로세스에 도달하는 열쇠는 실행 가능한 테스트 가능한 애플리케이션을 구축하는 데 있습니다. 이것은 다음 두 섹션에서 살펴볼 두 가지 관심사로 귀결됩니다. 첫 번째 관심사는 테스트 환경 과 관련이 있고 두 번째 관심사는 애플리케이션을 구성하는 방식과 관련이 있습니다.
왜 프레임워크가 필요한가?
소프트웨어(이하 Unit 이라고 함)를 테스트하려면 이 정의가 의도적으로 지나치게 단순화되었지만 런타임에 소프트웨어와 상호 작용할 수 있는 일종의 테스트 환경이 필요합니다. 순전히 JVM ( Java Virtual Machine ) 환경에서 실행되는 애플리케이션을 구축하는 경우 테스트를 작성하는 데 필요한 모든 것은 JRE ( Java Runtime Environment )입니다. 다음과 같은 매우 간단한 Calculator 클래스를 예로 들어 보겠습니다.
class Calculator { private int add(int a, int b){ return a + b; } private int subtract(int a, int b){ return a - b; } }
프레임워크가 없는 경우 실제로 코드를 실행하는 main
기능이 포함된 테스트 클래스만 있으면 테스트할 수 있습니다. 기억하시겠지만 main
함수는 간단한 Java 프로그램의 실행 시작점을 나타냅니다. 우리가 테스트하는 것에 관해서는, 우리는 단순히 몇 가지 테스트 데이터를 계산기의 기능에 공급하고 기본 산술이 제대로 수행되고 있는지 확인합니다.
public class Main { public static void main(String[] args){ //create a copy of the Unit to be tested Calculator calc = new Calculator(); //create test conditions to verify behaviour int addTest = calc.add(2, 2); int subtractTest = calc.subtract(2, 2); //verify behaviour by assertion if (addTest == 4) System.out.println("addTest has passed."); else System.out.println("addTest has failed."); if (subtractTest == 0) System.out.println("subtractTest has passed."); else System.out.println("subtractTest has failed."); } }
물론 Android 애플리케이션을 테스트하는 것은 완전히 다른 절차입니다. JVM 에서 Android 애플리케이션이 시작되기 전에 호출되는 ZygoteInit.java 파일(자세한 내용은 여기서 중요하지 않음)의 소스 깊숙이 묻힌 main
기능이 있지만 주니어 Android 개발자도 다음을 수행해야 합니다. 시스템 자체가 이 함수를 호출할 책임이 있음을 알고 있습니다. 개발자가 아닙니다 . 대신 Android 애플리케이션의 진입점은 Application
클래스와 AndroidManifest.xml 파일을 통해 시스템이 가리킬 수 있는 모든 Activity
클래스가 됩니다.
이 모든 것은 우리의 테스트 환경이 이제 Android 플랫폼을 고려해야 하기 때문에 엄격하게 말하면 Android 애플리케이션에서 테스트 단위 가 더 높은 수준의 복잡성을 나타낸다는 사실에 대한 단서일 뿐입니다.
타이트 커플링 문제 해결하기
긴밀한 결합 은 특정 플랫폼, 프레임워크, 언어 및 라이브러리에 의존 하는 기능, 클래스 또는 애플리케이션 모듈을 설명하는 용어입니다. 이것은 상대적인 용어입니다. 즉, Calculator.java 예제가 Java 프로그래밍 언어 및 표준 라이브러리에 밀접하게 결합되어 있지만 결합 범위는 여기까지입니다. 같은 맥락에서 Android 플랫폼과 밀접하게 연결된 클래스를 테스트하는 문제 는 플랫폼과 함께 또는 플랫폼 주변에서 작업할 방법을 찾아야 한다는 것입니다.
Android 플랫폼과 밀접하게 연결된 클래스의 경우 두 가지 옵션이 있습니다. 첫 번째는 클래스를 Android 장치(물리적 또는 가상)에 간단히 배포하는 것입니다. 프로덕션에 배포하기 전에 애플리케이션 코드를 테스트 배포하는 것이 좋지만 이는 개발 프로세스의 초기 및 중간 단계에서 시간과 관련하여 매우 비효율적인 접근 방식입니다.
Unit 은 당신이 선호하는 기술적인 정의이지만 일반적으로 클래스의 단일 함수로 생각됩니다(일부는 초기 단일 함수 호출에 의해 내부적으로 호출되는 후속 도우미 함수를 포함하도록 정의를 확장하지만). 어느 쪽이든 단위 는 작아야 합니다. 단일 단위 를 테스트하기 위해 전체 애플리케이션을 빌드, 컴파일 및 배포하는 것은 완전히 격리된 테스트의 요점을 놓치는 것입니다.
긴밀한 결합 문제에 대한 또 다른 솔루션은 테스트 프레임워크를 사용하여 플랫폼 종속성과 상호 작용하거나 모의 (시뮬레이션)하는 것입니다. Espresso 및 Robolectric 과 같은 프레임워크는 개발자에게 이전 접근 방식보다 훨씬 더 효과적인 단위 테스트 수단을 제공합니다. 전자는 장치에서 실행되는 테스트에 유용하고(장치 테스트를 호출하는 것이 충분히 모호하지 않기 때문에 "계측 테스트"라고 함) 후자는 JVM에서 로컬로 Android 프레임워크를 조롱할 수 있습니다.
내가 곧 논의할 대안 대신에 그러한 프레임워크에 대한 비난을 계속하기 전에, 이 옵션을 절대 사용해서는 안 된다는 의미가 아니라는 점을 분명히 하고 싶습니다. 개발자가 애플리케이션을 빌드하고 테스트하는 데 사용하는 프로세스는 개인의 선호도와 효율성에 대한 안목의 조합으로 탄생해야 합니다.
모듈식 및 느슨하게 결합된 애플리케이션을 구축하는 것을 좋아하지 않는 사람들을 위해 적절한 수준의 테스트 커버리지를 원한다면 이러한 프레임워크에 익숙해지는 것 외에 선택의 여지가 없습니다. 많은 훌륭한 응용 프로그램이 이러한 방식으로 구축되었으며 저는 응용 프로그램을 너무 모듈화되고 추상적으로 만든다는 비난을 자주 받지 않습니다. 당신이 내 접근 방식을 취하든 프레임워크에 크게 의존하기로 결정하든, 애플리케이션을 테스트하는 데 시간과 노력을 들인 것에 대해 경의를 표합니다.
팔 길이로 프레임 워크 유지
이 기사의 핵심 교훈에 대한 마지막 서문에서는 프레임워크 사용과 관련하여 미니멀리즘적 태도를 갖고 싶어하는 이유를 논의할 가치가 있습니다(이는 프레임워크 테스트 이상에 적용됨). 위의 부제는 소프트웨어 모범 사례의 관대한 교사인 Robert "Uncle Bob" C. Martin의 말을 의역한 것입니다. 내가 그의 작품을 처음 연구한 이후로 그가 나에게 준 많은 보석 중 이 보석은 이해하는 데 몇 년의 직접적인 경험이 필요했습니다.
이 진술의 내용을 이해하는 한 프레임워크 사용 비용은 프레임워크를 배우고 유지 관리하는 데 필요한 시간 투자입니다. 그들 중 일부는 매우 자주 변경되고 일부는 충분히 자주 변경되지 않습니다. 기능은 더 이상 사용되지 않으며 프레임워크는 더 이상 유지 관리되지 않으며 6-24개월마다 마지막 프레임워크를 대체하기 위해 새 프레임워크가 도착합니다. 따라서 플랫폼 또는 언어 기능(훨씬 더 오래 지속되는 경향이 있음)으로 구현할 수 있는 솔루션을 찾을 수 있다면 위에서 언급한 다양한 유형의 변경에 더 저항하는 경향이 있습니다.
좀 더 기술적으로 말하자면, Espresso 및 Robolectric 과 같은 프레임워크는 단순한 JUnit 테스트만큼 효율적으로 실행할 수 없으며 심지어 이전부터 프레임워크가 없는 테스트도 수행할 수 없습니다. JUnit 은 실제로 프레임워크이지만 적절한 Android 플랫폼보다 훨씬 느린 속도로 변경되는 경향이 있는 JVM 과 밀접하게 결합되어 있습니다. 적은 수의 프레임워크는 거의 예외 없이 하나 이상의 테스트를 실행하고 작성하는 데 걸리는 시간 측면에서 더 효율적인 코드를 의미합니다.
이를 통해 우리가 이제 Android 플랫폼을 무기력하게 유지할 수 있는 몇 가지 기술을 활용하는 접근 방식에 대해 논의할 것임을 알 수 있습니다. 많은 코드 커버리지, 테스트 효율성, 그리고 필요할 때 여기저기서 프레임워크를 계속 사용할 수 있는 기회를 제공합니다.
건축의 예술
어리석은 비유를 사용하면 프레임워크와 플랫폼을 적절한 경계를 설정하지 않는 한 개발 프로세스를 인수하는 위압적인 동료와 같다고 생각할 수 있습니다. 소프트웨어 아키텍처의 황금 원칙은 이러한 경계를 만들고 적용하는 데 필요한 일반적인 개념과 특정 기술을 제공할 수 있습니다. 잠시 후에 보게 되겠지만, 코드에 소프트웨어 아키텍처 원칙을 적용하는 것의 진정한 이점이 무엇인지 궁금하다면 일부는 직접적으로, 다수는 간접적으로 코드를 테스트하기 쉽게 만듭니다.
우려의 분리
관심사 분리 는 소프트웨어 아키텍처 전체에서 가장 보편적으로 적용 가능하고 유용한 개념입니다(다른 사람들은 무시해야 한다는 의미는 아님). 내가 알고 있는 소프트웨어 개발의 모든 관점에서 관심 분리(SOC)를 적용하거나 완전히 무시할 수 있습니다. 개념을 간략하게 요약하기 위해 클래스에 적용될 때 SOC를 살펴보지만 SOC는 도우미 함수 의 광범위한 사용을 통해 함수에 적용될 수 있으며 애플리케이션의 전체 모듈 ("모듈"에서 사용되는 "모듈")에 외삽될 수 있습니다. Android/Gradle의 컨텍스트).
GUI 응용 프로그램에 대한 소프트웨어 아키텍처 패턴을 연구하는 데 많은 시간을 보냈다면 Model-View-Controller(MVC), Model-View-Presenter(MVP) 또는 Model-View- 뷰모델(MVVM). 모든 스타일의 응용 프로그램을 구축한 후에는 그 중 어떤 것도 모든 프로젝트(또는 단일 프로젝트 내의 기능)에 대한 단일 최상의 옵션으로 간주하지 않는다는 점을 미리 말씀드립니다. 아이러니하게도 Android 팀이 몇 년 전에 권장하는 접근 방식인 MVVM으로 제시한 패턴은 Android 특정 테스트 프레임워크가 없는 경우 테스트하기 가장 어려운 것으로 보입니다(Android 플랫폼의 ViewModel 클래스를 사용하려는 가정). 의).
어쨌든 이러한 패턴의 세부 사항은 일반 사항보다 덜 중요합니다. 이 모든 패턴은 내가 Data , User Interface , Logic 이라고 하는 세 종류의 코드를 근본적으로 분리하는 것을 강조하는 SOC의 다른 특징일 뿐입니다.
그렇다면 Data , User Interface 및 Logic 을 분리하면 애플리케이션을 테스트하는 데 정확히 어떤 도움이 될까요? 대답은 플랫폼/프레임워크 종속성을 처리해야 하는 클래스에서 플랫폼/프레임워크 종속성이 거의 또는 전혀 없는 클래스로 논리를 끌어내면 테스트가 쉽고 프레임워크가 최소화 된다는 것입니다. 분명히 하자면, 나는 일반적으로 사용자 인터페이스를 렌더링하거나 SQL 테이블에 데이터를 저장하거나 원격 서버에 연결해야 하는 클래스에 대해 이야기하고 있습니다. 이것이 어떻게 작동하는지 보여주기 위해 가상의 Android 애플리케이션의 단순화된 3계층 아키텍처를 살펴보겠습니다.
첫 번째 클래스는 사용자 인터페이스를 관리합니다. 일을 단순하게 유지하기 위해 이 목적으로 활동 을 사용했지만 일반적으로 사용자 인터페이스 클래스 대신 Fragments 를 선택합니다. 두 경우 모두 Android 플랫폼과 유사한 긴밀한 결합을 나타냅니다.
public class CalculatorUserInterface extends Activity implements CalculatorContract.IUserInterface { private TextView display; private CalculatorContract.IControlLogic controlLogic; private final String INVALID_MESSAGE = "Invalid Expression."; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); controlLogic = new DependencyProvider().provideControlLogic(this); display = findViewById(R.id.textViewDisplay); Button evaluate = findViewById(R.id.buttonEvaluate); evaluate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { controlLogic.handleInput('='); } }); //..bindings for the rest of the calculator buttons } @Override public void updateDisplay(String displayText) { display.setText(displayText); } @Override public String getDisplay() { return display.getText().toString(); } @Override public void showError() { Toast.makeText(this, INVALID_MESSAGE, Toast.LENGTH_LONG).show(); } }
보시다시피 Activity 에는 두 가지 작업이 있습니다. 첫째, Android 애플리케이션의 지정된 기능에 대한 진입점이므로 기능의 다른 구성 요소에 대한 일종의 컨테이너 역할을 합니다. 간단히 말해서 컨테이너 는 다른 구성 요소가 참조(이 경우 개인 멤버 필드)를 통해 궁극적으로 연결되는 일종의 루트 클래스로 생각할 수 있습니다. 또한 참조를 확장하고 바인딩하고 XML 레이아웃(사용자 인터페이스)에 리스너를 추가합니다.
제어 로직 테스트
Activity 가 백엔드의 구체적인 클래스에 대한 참조를 소유하도록 하는 대신 CalculatorContract.IControlLogic.
이것이 인터페이스인 이유는 다음 섹션에서 논의할 것입니다. 지금은 해당 인터페이스의 반대편에 있는 것이 무엇이든 Presenter 또는 Controller 와 같은 것으로 가정된다는 점을 이해하십시오. 이 클래스는 프론트엔드 Activity 와 백엔드 Calculator 사이의 상호작용을 제어할 것이기 때문에 나는 그것을 CalculatorControlLogic
이라고 부르기로 선택했습니다.
public class CalculatorControlLogic implements CalculatorContract.IControlLogic { private CalculatorContract.IUserInterface ui; private CalculatorContract.IComputationLogic comp; public CalculatorControlLogic(CalculatorContract.IUserInterface ui, CalculatorContract.IComputationLogic comp) { this.ui = ui; this.comp = comp; } @Override public void handleInput(char inputChar) { switch (inputChar){ case '=': evaluateExpression(); break; //...handle other input events } } private void evaluateExpression() { Optional result = comp.computeResult(ui.getDisplay()); if (result.isPresent()) ui.updateDisplay(result.get()); else ui.showError(); } }
public class CalculatorControlLogic implements CalculatorContract.IControlLogic { private CalculatorContract.IUserInterface ui; private CalculatorContract.IComputationLogic comp; public CalculatorControlLogic(CalculatorContract.IUserInterface ui, CalculatorContract.IComputationLogic comp) { this.ui = ui; this.comp = comp; } @Override public void handleInput(char inputChar) { switch (inputChar){ case '=': evaluateExpression(); break; //...handle other input events } } private void evaluateExpression() { Optional result = comp.computeResult(ui.getDisplay()); if (result.isPresent()) ui.updateDisplay(result.get()); else ui.showError(); } }
이 클래스가 디자인된 방식에 대해 테스트하기 쉽게 만드는 미묘한 부분이 많이 있습니다. 첫째, 모든 참조는 Java 표준 라이브러리 또는 애플리케이션 내에 정의된 인터페이스에서 가져온 것입니다. 이것은 프레임워크 없이 이 클래스를 테스트하는 것이 절대 쉬운 일이며 JVM 에서 로컬로 수행될 수 있음을 의미합니다. 작지만 유용한 또 다른 팁은 이 클래스의 다른 모든 상호 작용이 단일 일반 handleInput(...)
함수를 통해 호출될 수 있다는 것입니다. 이것은 이 클래스의 모든 동작을 테스트하기 위한 단일 진입점 을 제공합니다.
또한 evaluateExpression()
함수에서 백엔드에서 Optional<String>
유형의 클래스를 반환한다는 점에 유의하십시오. 일반적으로 저는 함수형 프로그래머가 Even Monad 라고 부르는 것을 사용하거나 선호하는 대로 Result Wrapper 라고 부릅니다. 어떤 어리석은 이름을 사용하든 단일 함수 호출을 통해 여러 다른 상태를 나타낼 수 있는 개체입니다. Optional
은 null 또는 제공된 제네릭 유형의 일부 값을 나타낼 수 있는 더 간단한 구문입니다. 어쨌든 백엔드에 잘못된 표현식이 제공될 수 있으므로 ControlLogic
클래스에 백엔드 작업의 결과를 결정하는 수단을 제공하고자 합니다. 성공과 실패 모두를 설명합니다. 이 경우 null 은 실패를 나타냅니다.
다음은 JUnit 을 사용하여 작성된 테스트 클래스의 예이며 테스트 전문 용어에서 Fake 라고 하는 클래스입니다.
public class CalculatorControlLogicTest { @Test public void validExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); CalculatorContract.IUserInterface ui = new FakeUserInterface(); CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).displayUpdateCalled); assertTrue(((FakeUserInterface) ui).displayValueFinal.equals("10.0")); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } @Test public void invalidExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); ((FakeComputationLogic) comp).returnEmpty = true; CalculatorContract.IUserInterface ui = new FakeUserInterface(); ((FakeUserInterface) ui).displayValueInitial = "+7+7"; CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).showErrorCalled); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } private class FakeUserInterface implements CalculatorContract.IUserInterface{ boolean displayUpdateCalled = false; boolean showErrorCalled = false; String displayValueInitial = "5+5"; String displayValueFinal = ""; @Override public void updateDisplay(String displayText) { displayUpdateCalled = true; displayValueFinal = displayText; } @Override public String getDisplay() { return displayValueInitial; } @Override public void showError() { showErrorCalled = true; } } private class FakeComputationLogic implements CalculatorContract.IComputationLogic{ boolean computeResultCalled = false; boolean returnEmpty = false; @Override public Optional computeResult(String expression) { computeResultCalled = true; if (returnEmpty) return Optional.empty(); else return Optional.of("10.0"); } } }
public class CalculatorControlLogicTest { @Test public void validExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); CalculatorContract.IUserInterface ui = new FakeUserInterface(); CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).displayUpdateCalled); assertTrue(((FakeUserInterface) ui).displayValueFinal.equals("10.0")); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } @Test public void invalidExpressionTest() { CalculatorContract.IComputationLogic comp = new FakeComputationLogic(); ((FakeComputationLogic) comp).returnEmpty = true; CalculatorContract.IUserInterface ui = new FakeUserInterface(); ((FakeUserInterface) ui).displayValueInitial = "+7+7"; CalculatorControlLogic controller = new CalculatorControlLogic(ui, comp); controller.handleInput('='); assertTrue(((FakeUserInterface) ui).showErrorCalled); assertTrue(((FakeComputationLogic) comp).computeResultCalled); } private class FakeUserInterface implements CalculatorContract.IUserInterface{ boolean displayUpdateCalled = false; boolean showErrorCalled = false; String displayValueInitial = "5+5"; String displayValueFinal = ""; @Override public void updateDisplay(String displayText) { displayUpdateCalled = true; displayValueFinal = displayText; } @Override public String getDisplay() { return displayValueInitial; } @Override public void showError() { showErrorCalled = true; } } private class FakeComputationLogic implements CalculatorContract.IComputationLogic{ boolean computeResultCalled = false; boolean returnEmpty = false; @Override public Optional computeResult(String expression) { computeResultCalled = true; if (returnEmpty) return Optional.empty(); else return Optional.of("10.0"); } } }
보시다시피, 이 테스트 스위트는 매우 빠르게 실행할 수 있을 뿐만 아니라 작성하는 데 많은 시간이 걸리지 않습니다. 어쨌든, 우리는 이제 이 테스트 클래스를 매우 쉽게 작성할 수 있도록 하는 좀 더 미묘한 것들을 살펴볼 것입니다.
추상화와 의존성 반전의 힘
CalculatorControlLogic
에 적용된 두 가지 중요한 개념을 통해 테스트를 매우 쉽게 만들 수 있습니다. 첫째, Java에서 인터페이스 및 추상 클래스 (집합적으로 추상화 라고 함)를 사용하는 이점이 무엇인지 궁금하다면 위의 코드가 직접적인 데모입니다. 테스트할 클래스는 구체적인 클래스 대신 추상화 를 참조하기 때문에 테스트 클래스 내에서 사용자 인터페이스 와 백엔드 에 대한 가짜 테스트 더블을 생성할 수 있었습니다. 이러한 테스트 더블이 적절한 인터페이스를 구현하는 한 CalculatorControlLogic
은 실제 인터페이스가 아님을 신경 쓰지 않을 수 없습니다.
둘째, CalculatorControlLogic
은 자체 종속성을 생성하는 대신 생성자를 통해 종속성을 제공받았습니다(예, 종속성 주입 의 한 형태임). 따라서 프로덕션 또는 테스트 환경에서 사용할 때 다시 작성할 필요가 없으므로 효율성이 향상됩니다.
종속성 주입 은 Inversion Of Control 의 한 형태로, 일반 언어로 정의하기 어려운 개념입니다. 의존성 주입 을 사용하든 서비스 로케이터 패턴 을 사용하든, 둘 다 Martin Fowler(이러한 주제에 대해 내가 가장 좋아하는 교사)가 "구성과 사용을 분리하는 원칙"이라고 설명하는 것을 달성합니다. 그 결과 테스트하기 쉽고 서로 분리하여 빌드하기 더 쉬운 클래스가 생성됩니다.
계산 논리 테스트
마지막으로 원격 서버 또는 로컬 데이터베이스에 대한 어댑터와 같은 IO 장치 를 근사화해야 하는 ComputationLogic
클래스가 있습니다. 간단한 계산기에는 둘 다 필요하지 않기 때문에 우리가 제공하는 표현식의 유효성을 검사하고 평가하는 데 필요한 논리를 캡슐화하는 역할만 합니다.
public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } }
public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } }
public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } }
public class CalculatorComputationLogic implements CalculatorContract.IComputationLogic { private final char ADD = '+'; private final char SUBTRACT = '-'; private final char MULTIPLY = '*'; private final char DIVIDE = '/'; @Override public Optional computeResult(String expression) { if (hasOperator(expression)) return attemptEvaluation(expression); else return Optional.empty(); } private Optional attemptEvaluation(String expression) { String delimiter = getOperator(expression); Binomial b = buildBinomial(expression, delimiter); return evaluateBinomial(b); } private Optional evaluateBinomial(Binomial b) { String result; switch (b.getOperatorChar()) { case ADD: result = Double.toString(b.firstTerm + b.secondTerm); break; case SUBTRACT: result = Double.toString(b.firstTerm - b.secondTerm); break; case MULTIPLY: result = Double.toString(b.firstTerm * b.secondTerm); break; case DIVIDE: result = Double.toString(b.firstTerm / b.secondTerm); break; default: return Optional.empty(); } return Optional.of(result); } private Binomial buildBinomial(String expression, String delimiter) { String[] operands = expression.split(delimiter); return new Binomial( delimiter, Double.parseDouble(operands[0]), Double.parseDouble(operands[1]) ); } private String getOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return "\\" + c; } //default return "+"; } private boolean hasOperator(String expression) { for (char c : expression.toCharArray()) { if (c == ADD || c == SUBTRACT || c == MULTIPLY || c == DIVIDE) return true; } return false; } private class Binomial { String operator; double firstTerm; double secondTerm; Binomial(String operator, double firstTerm, double secondTerm) { this.operator = operator; this.firstTerm = firstTerm; this.secondTerm = secondTerm; } char getOperatorChar(){ return operator.charAt(operator.length() - 1); } } }
일반적으로 Android에 밀접하게 결합된 클래스와 유사한 문제를 나타내는 특정 백엔드 라이브러리에 대한 일부 긴밀한 결합이 있기 때문에 이 클래스에 대해 할 말이 많지 않습니다. 잠시 후 이러한 클래스에 대해 무엇을 해야 하는지 논의할 것이지만, 이 클래스는 테스트하기가 너무 쉽기 때문에 시도해 볼 수도 있습니다.
public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } }
public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } }
public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } }
public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } }
public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } }
public class CalculatorComputationLogicTest { private CalculatorComputationLogic comp = new CalculatorComputationLogic(); @Test public void additionTest() { String EXPRESSION = "5+5"; String ANSWER = "10.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void subtractTest() { String EXPRESSION = "5-5"; String ANSWER = "0.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void multiplyTest() { String EXPRESSION = "5*5"; String ANSWER = "25.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void divideTest() { String EXPRESSION = "5/5"; String ANSWER = "1.0"; Optional result = comp.computeResult(EXPRESSION); assertTrue(result.isPresent()); assertEquals(result.get(), ANSWER); } @Test public void invalidTest() { String EXPRESSION = "Potato"; Optional result = comp.computeResult(EXPRESSION); assertTrue(!result.isPresent()); } }
테스트하기 가장 쉬운 클래스는 단순히 값이나 객체가 주어지며 외부 종속성을 호출할 필요 없이 결과를 반환할 것으로 예상되는 클래스입니다. 어쨌든 소프트웨어 아키텍처 마법사를 아무리 많이 적용하더라도 플랫폼 및 프레임워크에서 분리할 수 없는 클래스에 대해 여전히 걱정해야 하는 시점이 옵니다. 다행히도 소프트웨어 아키텍처를 다음과 같이 사용할 수 있는 방법이 있습니다. 최악의 경우 이러한 클래스를 테스트하기 쉽게 만들고, 기껏해야 테스트 를 한 눈에 수행할 수 있을 정도로 간단하게 만듭니다.
겸손한 대상과 수동적 견해
위의 두 이름은 하위 수준 종속성과 대화해야 하는 개체가 테스트할 필요가 없을 정도로 단순화 된 패턴을 나타냅니다. 나는 Model-View-Presenter의 변형에 대한 Martin Fowler의 블로그를 통해 이 패턴을 처음 소개받았습니다. 나중에 Robert C. Martin의 작업을 통해 나는 특정 클래스를 Humble Objects 로 취급한다는 아이디어를 소개받았습니다. 이는 이 패턴이 사용자 인터페이스 클래스로 제한될 필요가 없음을 의미합니다(하지만 Fowler가 이제까지 그러한 제한을 암시함).
이 패턴을 무엇이라고 부르든 이해하기 매우 쉽고 어떤 의미에서는 SOC를 수업에 엄격하게 적용한 결과라고 생각합니다. 이 패턴은 백엔드 클래스에도 적용되지만 사용자 인터페이스 클래스를 사용하여 이 원칙을 실제로 시연할 것입니다. 분리는 매우 간단합니다. 플랫폼 및 프레임워크 종속성과 상호 작용하는 클래스는 스스로 생각하지 않습니다(따라서 Humble 및 Passive 라는 별명). 이벤트가 발생하면 그들이 하는 유일한 일은 이 이벤트의 세부 정보를 수신하는 논리 클래스에 전달하는 것뿐입니다.
//from CalculatorActivity's onCreate() function: evaluate.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { controlLogic.handleInput('='); } });
테스트하기가 매우 쉬워야 하는 논리 클래스는 매우 세밀한 방식으로 사용자 인터페이스 를 제어하는 일을 담당합니다. user interface
클래스에서 단일 일반 updateUserInterface(...)
함수를 호출하고 대량 업데이트 작업을 수행하도록 남겨두는 대신 user interface
(또는 기타 해당 클래스)는 사용하기 쉬워야 하는 작고 특정한 함수를 소유하게 됩니다. 이름과 구현:
//Interface functions of CalculatorActivity: @Override public void updateDisplay(String displayText) { display.setText(displayText); } @Override public String getDisplay() { return display.getText().toString(); } @Override public void showError() { Toast.makeText(this, INVALID_MESSAGE, Toast.LENGTH_LONG).show(); } //…
원칙적으로 이 두 가지 예는 이 패턴을 구현하는 방법을 이해하기에 충분해야 합니다. 논리를 가지고 있는 객체는 느슨하게 결합되고, 성가신 종속성에 밀접하게 결합된 객체는 논리가 거의 없게 됩니다.
Now, at the start of this subsection, I made the statement that these classes become arguably unnecessary to test, and it is important we look at both sides of this argument. In an absolute sense, it is impossible to achieve 100% test coverage by employing this pattern, unless you still write tests for such humble / passive classes. It is also worth noting that my decision to use a Calculator as an example App, means that I cannot escape having a gigantic mass of findViewById(...)
calls present in the Activity. Giant masses of repetitive code are a common cause of typing errors, and in the absence of some Android UI testing frameworks, my only recourse for testing would be via deploying the feature to a device and manually testing each interaction. 아야.
It is at this point that I will humbly say that I do not know if 100% code coverage is absolutely necessary. I do not know many developers who strive for absolute test coverage in production code, and I have never done so myself. One day I might, but I will reserve my opinions on this matter until I have the reference experiences to back them up. In any case, I would argue that applying this pattern will still ultimately make it simpler and easier to test tightly coupled classes; if for no reason other than they become simpler to write.
Another objection to this approach, was raised by a fellow programmer when I described this approach in another context. The objection was that the logic
class (whether it be a Controller
, Presenter
, or even a ViewModel
depending on how you use it), becomes a God
class.
While I do not agree with that sentiment, I do agree that the end result of applying this pattern is that your Logic classes become larger than if you left more decisions up to your user interface class.
This has never been an issue for me as I treat each feature of my applications as self-contained components, as opposed to having one giant controller for managing multiple user interface screens. In any case, I think this argument holds reasonably true if you fail to apply SOC to your front end or back end components. Therefore, my advice is to apply SOC to your front end and back end components quite rigorously.
추가 고려 사항
After all of this discussion on applying the principles of software architecture to reduce the necessity of using a wide-array of testing frameworks, improve the testability of classes in general, and a pattern which allows classes to be tested indirectly (at least to some degree), I am not actually here to tell you to stop using your preferred frameworks.
For those curious, I often use a library to generate mock classes for my Unit tests (for Java I prefer Mockito , but these days I mostly write Kotlin and prefer Mockk in that language), and JUnit is a framework which I use quite invariably. Since all of these options are coupled to languages as opposed to the Android platform, I can use them quite interchangeably across mobile and web application development. From time to time (if project requirements demand it), I will even use tools like Robolectric , MockWebServer , and in my five years of studying Android, I did begrudgingly use Espresso once.
My hope is that in reading this article, anyone who has experienced a similar degree of aversion to testing due to paralysis by jargon analysis , will come to see that getting started with testing really can be simple and framework minimal .
SmashingMag에 대한 추가 정보:
- Sliding In And Out Of Vue.js
- Designing And Building A Progressive Web Application Without A Framework
- CSS 프레임워크 또는 CSS 그리드: 내 프로젝트에 무엇을 사용해야 합니까?
- 진정한 크로스 플랫폼 모바일 개발을 위한 Google의 Flutter 사용