Slide 1

Slide 1 text

Unit Test your code, even your UI @stan_kocken

Slide 2

Slide 2 text

Testing Instrumentation (on Android Device) Unit Tests (on JVM)

Slide 3

Slide 3 text

Unit Testing on JVM Run quickly Allow TDD (Test Driven Development) Easy to add on your build process (CI)

Slide 4

Slide 4 text

app ↳ src ↳ main ↳ test ↳ java ↳ com.skocken.junittest ↳ ExampleUnitTest Test hierarchy

Slide 5

Slide 5 text

Build Variants Android Studio Configure View > Tools Windows > Test Artifact: Android Instrumentation Tests Unit Tests

Slide 6

Slide 6 text

Android Studio Execute app ↳ src ↳ main ↳ test ↳ java ↳ com.skocken.junittest ↳ ExampleUnitTest Right click > Run ‘All tests’ (or Run ‘Junit’)

Slide 7

Slide 7 text

Gradle Execute ./gradlew testDebugUnitTest … BUILD SUCCESSFUL Total time: 2.057 secs test{flavor}{build_type}UnitTest

Slide 8

Slide 8 text

app ↳ src ↳ main ↳ test ↳ java ↳ com.skocken.junittest ↳ ExampleUnitTest public class ExampleUnitTest {
 @Test
 public void addition_isCorrect() throws Exception {
 assertEquals(4, 2 + 2);
 }
 } OK

Slide 9

Slide 9 text

TDD Add a test Run (should fail) Write code to fix it Run (should succeed) Refactor code (Test Driven Development)

Slide 10

Slide 10 text

Basic class public class Plane {
 
 private final String mName;
 
 public Plane(String name) {
 mName = name;
 }
 
 } Alt + Enter > Create Test > JUnit4

Slide 11

Slide 11 text

Basic test public class PlaneTest {
 
 
 } 
 @Test
 public void testShouldCompareName() {
 Plane plane = new Plane("A380");
 assertTrue(plane.sameAs("A380"));
 assertFalse(plane.sameAs("B747"));
 } Alt + Enter > Create method ‘sameAs’

Slide 12

Slide 12 text

Add ‘sameAs’ public class Plane {
 
 private final String mName;
 
 public Plane(String name) {
 mName = name;
 } 
 
 } 
 public boolean sameAs(String name) {
 return false;
 }


Slide 13

Slide 13 text

Run Test public class PlaneTest { 
 @Test
 public void testShouldCompareName() {
 Plane plane = new Plane("A380");
 assertTrue(plane.sameAs("A380"));
 assertFalse(plane.sameAs("B747"));
 } } !

Slide 14

Slide 14 text

Fix ‘sameAs’ public class Plane {
 
 private final String mName;
 
 public Plane(String name) {
 mName = name;
 } 
 public boolean sameAs(String name) {
 return false; 
 }
 
 } // return false; return name != null && name.equals(mName);

Slide 15

Slide 15 text

Run Test public class PlaneTest { 
 @Test
 public void testShouldCompareName() {
 Plane plane = new Plane("A380");
 assertTrue(plane.sameAs("A380"));
 assertFalse(plane.sameAs("B747"));
 } } OK

Slide 16

Slide 16 text

Add some Android logs public class Plane {
 
 private final String mName;
 
 public Plane(String name) {
 mName = name;
 } 
 public boolean sameAs(String name) { 
 return name != null && name.equals(mName);
 }
 
 } Log.v("Plane", "compare with: " + name);

Slide 17

Slide 17 text

Run Test public class PlaneTest { 
 @Test
 public void testShouldCompareName() {
 Plane plane = new Plane("A380");
 assertTrue(plane.sameAs("A380"));
 assertFalse(plane.sameAs("B747"));
 } } ! java.lang.RuntimeException: Method v in android.util.Log not mocked. at android.util.Log.v(Log.java) at com.skocken.junittest.Plane.sameAs(Plane.java:14) at com.skocken.junittest.PlaneTest.testShouldCompareName(PlaneTest.java:13)

Slide 18

Slide 18 text

Mock android.jar - build.gradle android {
 … testOptions {
 unitTests.returnDefaultValues = true
 }
 }

Slide 19

Slide 19 text

Run Test public class PlaneTest { 
 @Test
 public void testShouldCompareName() {
 Plane plane = new Plane("A380");
 assertTrue(plane.sameAs("A380"));
 assertFalse(plane.sameAs("B747"));
 } } OK

Slide 20

Slide 20 text

Run Test, with “null” public class PlaneTest {
 
 @Test
 public void testShouldCompareName() { … }
 
 @Test
 public void testShouldCompareNameNull() {
 Plane plane = new Plane(null);
 assertTrue(plane.sameAs(null));
 assertFalse(plane.sameAs("B747"));
 }
 
 } !

Slide 21

Slide 21 text

Fix ‘sameAs’ public class Plane {
 
 private final String mName;
 
 public Plane(String name) {
 mName = name;
 } 
 public boolean sameAs(String name) { Log.v("Plane", "compare with: " + name);
 return name != null && name.equals(mName); 
 }
 
 } // return name != null && name.equals(mName); return TextUtils.equals(mName, name);

Slide 22

Slide 22 text

Run Test public class PlaneTest {
 
 @Test
 public void testShouldCompareName() { Plane plane = new Plane("A380");
 assertTrue(plane.sameAs("A380"));
 assertFalse(plane.sameAs(“B747")); }
 
 @Test
 public void testShouldCompareNameNull() {
 Plane plane = new Plane(null);
 assertTrue(plane.sameAs(null));
 assertFalse(plane.sameAs("B747"));
 }
 
 } !

Slide 23

Slide 23 text

build.gradle android {
 … testOptions {
 unitTests.returnDefaultValues = true
 }
 } public static boolean equals(CharSequence a, CharSequence b) {
 …
 } public static boolean equals(CharSequence a, CharSequence b) {
 return false;
 } TextUtils.java Mock of TextUtils.java

Slide 24

Slide 24 text

Solutions TextUtils → custom method (or library like Apache Common) Need to replace every calls More code “duplicate” “just because” of the test Tests clean, no change

Slide 25

Slide 25 text

Solutions Robolectric Not “exactly” Android A bit slower (~ +3sec) No change on code to test

Slide 26

Slide 26 text

Example

Slide 27

Slide 27 text

Board play(Player player, int x, int y) Player getPlayer(int x, int y) int getPlayLeft() Player getWinner()

Slide 28

Slide 28 text

Tests public class GameBoardTest extends TestCase { 
 private GameBoard mSubject; 
 @Before
 public void setUp() throws Exception {
 mSubject = new GameBoard();
 } 
 @Test
 public void testSetPlayer() throws Exception { assertEquals(GameBoard.Player.EMPTY, mSubject.getPlayer(0, 0)); mSubject.play(GameBoard.Player.J1, 0, 0); assertEquals(GameBoard.Player.J1, mSubject.getPlayer(0, 0));
 } 
 } Fix your tests now

Slide 29

Slide 29 text

Tests public class GameBoardTest extends TestCase {
 
 private GameBoard mSubject;
 
 @Before
 public void setUp() throws Exception {
 mSubject = new GameBoard();
 }
 
 @Test
 public void testSetPlayer() throws Exception { … }
 
 @Test
 public void testGetNbPlayLeft() throws Exception {
 assertEquals(9, mSubject.getNbPlayLeft());
 mSubject.play(GameBoard.Player.J1, 2, 2);
 assertEquals(8, mSubject.getNbPlayLeft());
 mSubject.play(GameBoard.Player.J1, 1, 1);
 assertEquals(7, mSubject.getNbPlayLeft());
 }
 
 } Fix your tests now

Slide 30

Slide 30 text

Tests public class GameBoardTest extends TestCase {
 
 private GameBoard mSubject;
 
 @Before
 public void setUp() throws Exception {
 mSubject = new GameBoard();
 }
 
 …
 
 @Test
 public void testGetWinnerInCol() throws Exception {
 mSubject.play(GameBoard.Player.J1, 0, 0);
 mSubject.play(GameBoard.Player.J1, 0, 1);
 assertNull(mSubject.getWinner());
 mSubject.play(GameBoard.Player.J1, 0, 2);
 assertEquals(mSubject.getWinner(), GameBoard.Player.J1);
 }
 
 } Fix your tests now

Slide 31

Slide 31 text

AI constructor: (GameBoard board, Player aiPlayer) playNext()

Slide 32

Slide 32 text

Tests public class AITest extends TestCase {
 
 private static final GameBoard.Player PLAYER_AI = GameBoard.Player.J2;
 
 private GameBoard mGameBoard;
 
 private AI mAI;
 
 @Before
 public void setUp() throws Exception {
 mGameBoard = new GameBoard();
 mAI = new AI(mGameBoard, PLAYER_AI);
 }
 
 }

Slide 33

Slide 33 text

Tests public class AITest extends TestCase { 
 …
 Fix your tests now @Test
 public void testPlayNextWithEmpty() throws Exception {
 GameBoard.Player[][] initBoard =
 {
 {__, __, __},
 {__, __, __},
 {__, __, __}
 
 };
 mGameBoard.setBoard(initBoard);
 mAI.playNext(); assertEquals(PLAYER_AI, mGameBoard.getPlayer(1, 1));
 }
 
 }

Slide 34

Slide 34 text

Tests @Test
 public void testPlayWinnerMove() throws Exception {
 GameBoard.Player[][] initBoard =
 {
 {J1, IA, J1},
 {IA, IA, J1},
 {J1, __, __}
 
 };
 mGameBoard.setBoard(initBoard);
 
 mIA.playNext(); 
 assertEquals(IA, mGameBoard.getPlayer(1, 2));
 assertEquals(IA, mGameBoard.getWinner());
 }
 
 } Fix your tests now public class AITest extends TestCase { 
 …


Slide 35

Slide 35 text

Go Further Improve your AI Increase board size …

Slide 36

Slide 36 text

Android UI

Slide 37

Slide 37 text

Activity

Slide 38

Slide 38 text

BaseActivity:1600+ lines A base activity that handles common functionality in the app. This includes the navigation drawer, login and authentication, Action Bar tweaks, amongst others.

Slide 39

Slide 39 text

God Object

Slide 40

Slide 40 text

How to do differently? View Proxy Touch Android View No business logic DataProvider Access Data Store state Presenter Data to ViewProxy

Slide 41

Slide 41 text

Architecture Presenter DataProvider ViewProxy Activity View Strong Reference Weak Reference Other “Strong” Reference on DataProvider, Presenter or ViewProxy forbidden. Use WeakReference. OK

Slide 42

Slide 42 text

Example

Slide 43

Slide 43 text

v1: Activity public class OriginalActivity extends Activity
 implements View.OnClickListener {
 
 private final GameBoard mGameBoard = new GameBoard();
 
 private final GameBoard.Player mCurrentPlayer = GameBoard.Player.J1;
 
 private final AI mAI = new AI(mGameBoard, GameBoard.Player.J2);
 
 private ViewGroup mBoxesLayout;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 mBoxesLayout = (ViewGroup) findViewById(R.id.boxes_layout);
 }
 
 @Override
 protected void onResume() {
 super.onResume();
 refreshBoard();
 }
 
 @Override
 public void onClick(View v) {
 Object x = v.getTag(R.id.tag_x);
 Object y = v.getTag(R.id.tag_y);
 if (x instanceof Integer && y instanceof Integer) {
 play((int) x, (int) y);
 }
 }
 
 private void play(int x, int y) {
 mGameBoard.play(mCurrentPlayer, x, y);
 mAI.playNext();
 refreshBoard();
 } private void refreshBoard() {
 int size = mGameBoard.getSize();
 for (int y = 0; y < size; y++) {
 for (int x = 0; x < size; x++) {
 GameBoard.Player player = mGameBoard.getPlayer(x, y);
 
 int boxType;
 switch (player) {
 case EMPTY:
 boxType = BoardDef.BOX_EMPTY;
 break;
 case J1:
 boxType = BoardDef.BOX_CROSS;
 break;
 case J2:
 boxType = BoardDef.BOX_ROUND;
 break;
 default:
 continue;
 }
 setBoxValue(x, y, boxType);
 }
 }
 }
 
 private void setBoxValue(int x, int y, int boxType) {
 ViewGroup rowLayout = (ViewGroup) mBoxesLayout.getChildAt(y);
 View boxView = rowLayout.getChildAt(x);
 
 int bgColor;
 switch (boxType) {
 case BoardDef.BOX_CROSS:
 bgColor = Color.RED;
 boxView.setOnClickListener(null);
 break;
 case BoardDef.BOX_ROUND:
 bgColor = Color.BLUE;
 boxView.setOnClickListener(null);
 break;
 default:
 bgColor = Color.GRAY;
 boxView.setOnClickListener(this);
 break;
 }
 boxView.setBackgroundColor(bgColor);
 boxView.setTag(R.id.tag_x, x);
 boxView.setTag(R.id.tag_y, y);
 }
 }

Slide 44

Slide 44 text

DataProvider Presenter ViewProxy v2: Creation refreshBoard() getGameBoard() setBoxValue( int x, int y, int boxType);

Slide 45

Slide 45 text

DataProvider Presenter ViewProxy v2: User click box onSelectBox(int x, int y) play(int x, int y) refreshBoard() …

Slide 46

Slide 46 text

v2: Definition public interface BoardDef {
 
 int BOX_EMPTY = 0;
 
 int BOX_CROSS = 1;
 
 int BOX_ROUND = 2;
 
 interface IPresenter extends BaseDef.IPresenter {
 
 void refreshBoard();
 
 void onSelectBox(int x, int y);
 }
 
 interface IDataProvider extends BaseDef.IDataProvider {
 
 GameBoard getGameBoard();
 
 void play(int x, int y);
 }
 
 interface IView extends BaseDef.IView {
 
 void setBoxValue(int x, int y, int boxType);
 }
 }

Slide 47

Slide 47 text

v2: DataProvider public class BoardDataProvider
 extends BaseDataProvider
 implements BoardDef.IDataProvider {
 
 private final GameBoard mGameBoard = new GameBoard();
 
 private final GameBoard.Player mCurrentPlayer = GameBoard.Player.J1;
 
 private final AI mAI = new AI(mGameBoard, GameBoard.Player.J2);
 
 @Override
 public GameBoard getGameBoard() {
 return mGameBoard;
 }
 
 @Override
 public void play(int x, int y) {
 mGameBoard.play(mCurrentPlayer, x, y);
 mAI.playNext();
 getPresenter().refreshBoard();
 }
 }


Slide 48

Slide 48 text

public class BoardPresenter 
 extends BasePresenter
 implements BoardDef.IPresenter {
 
 public BoardPresenter(Activity activity,
 BoardDef.IDataProvider provider, BoardDef.IView view) {
 super(activity, provider, view);
 }
 
 @Override
 public void onResume() {
 super.onResume();
 refreshBoard();
 }
 
 @Override
 public void onSelectBox(int x, int y) {
 getProvider().play(x, y);
 }
 
 @Override
 public void refreshBoard() {
 GameBoard gameBoard = getProvider().getGameBoard();
 int size = gameBoard.getSize();
 for (int y = 0; y < size; y++) {
 for (int x = 0; x < size; x++) {
 setBoxValue(gameBoard, y, x);
 }
 }
 } v2: Presenter

Slide 49

Slide 49 text

public class BoardPresenter 
 extends BasePresenter
 implements BoardDef.IPresenter {
 
 public BoardPresenter(Activity activity,
 BoardDef.IDataProvider provider, BoardDef.IView view) {
 super(activity, provider, view);
 }
 
 @Override
 public void onResume() {
 super.onResume();
 refreshBoard();
 }
 
 @Override
 public void onSelectBox(int x, int y) {
 getProvider().play(x, y);
 }
 
 @Override
 public void refreshBoard() {
 GameBoard gameBoard = getProvider().getGameBoard();
 int size = gameBoard.getSize();
 for (int y = 0; y < size; y++) {
 for (int x = 0; x < size; x++) {
 setBoxValue(gameBoard, y, x);
 }
 }
 } v2: Presenter private void setBoxValue(GameBoard gameBoard, int y, int x) {
 GameBoard.Player player = gameBoard.getPlayer(x, y);
 int boxValue;
 switch (player) {
 case EMPTY:
 boxValue = BoardDef.BOX_EMPTY;
 break;
 case J1:
 boxValue = BoardDef.BOX_CROSS;
 break;
 case J2:
 boxValue = BoardDef.BOX_ROUND;
 break;
 default:
 return;
 }
 getView().setBoxValue(x, y, boxValue);
 }
 }

Slide 50

Slide 50 text

public class BoardViewProxy extends BaseViewProxy
 implements BoardDef.IView, View.OnClickListener {
 
 public BoardViewProxy(Activity activity) {
 super(activity);
 }
 
 @Override
 public void onClick(View v) {
 int x = (Integer) v.getTag(R.id.tag_x);
 int y = (Integer) v.getTag(R.id.tag_y);
 getPresenter().onSelectBox(x, y);
 } v2: ViewProxy @Override
 public void setBoxValue(int x, int y, int boxType) {
 ViewGroup boxLayout = findViewByIdEfficient(R.id.boxes_layout);
 ViewGroup rowLayout = (ViewGroup) boxLayout.getChildAt(y);
 View boxView = rowLayout.getChildAt(x);
 
 int bgColor;
 switch (boxType) {
 case BoardDef.BOX_CROSS:
 bgColor = Color.RED;
 boxView.setOnClickListener(null);
 break;
 case BoardDef.BOX_ROUND:
 bgColor = Color.BLUE;
 boxView.setOnClickListener(null);
 break;


Slide 51

Slide 51 text

public class BoardViewProxy extends BaseViewProxy
 implements BoardDef.IView, View.OnClickListener {
 
 public BoardViewProxy(Activity activity) {
 super(activity);
 }
 
 @Override
 public void onClick(View v) {
 int x = (Integer) v.getTag(R.id.tag_x);
 int y = (Integer) v.getTag(R.id.tag_y);
 getPresenter().onSelectBox(x, y);
 } v2: ViewProxy @Override
 public void setBoxValue(int x, int y, int boxType) {
 ViewGroup boxLayout = findViewByIdEfficient(R.id.boxes_layout);
 ViewGroup rowLayout = (ViewGroup) boxLayout.getChildAt(y);
 View boxView = rowLayout.getChildAt(x);
 
 int bgColor;
 switch (boxType) {
 case BoardDef.BOX_CROSS:
 bgColor = Color.RED;
 boxView.setOnClickListener(null);
 break;
 case BoardDef.BOX_ROUND:
 bgColor = Color.BLUE;
 boxView.setOnClickListener(null);
 break;
 default:
 bgColor = Color.GRAY;
 boxView.setOnClickListener(this);
 break;
 }
 boxView.setBackgroundColor(bgColor);
 boxView.setTag(R.id.tag_x, x);
 boxView.setTag(R.id.tag_y, y);
 } }

Slide 52

Slide 52 text

Improve clarity To test it easily! Why split my god object Activity?

Slide 53

Slide 53 text

Tests: setup public class BoardDataProviderTest extends TestCase {
 
 private BoardDef.IPresenter mPresenter;
 
 private BoardDataProvider mDataProvider;
 
 @Before
 public void setUp() throws Exception {
 mPresenter = Mockito.mock(BoardDef.IPresenter.class);
 
 mDataProvider = new BoardDataProvider();
 mDataProvider.setPresenter(mPresenter);
 }

Slide 54

Slide 54 text

Tests: setup public class BoardPresenterTest extends TestCase {
 
 private BoardDef.IDataProvider mDataProvider;
 
 private BoardDef.IView mBoardView;
 
 private BoardPresenter mBoardPresenter;
 
 @Before
 public void setUp() throws Exception {
 mDataProvider = Mockito.mock(BoardDef.IDataProvider.class);
 mBoardView = Mockito.mock(BoardDef.IView.class);
 
 mBoardPresenter = new BoardPresenter(mDataProvider, mBoardView);
 }

Slide 55

Slide 55 text

Tests: setup public class BoardViewProxyTest extends TestCase {
 
 private View mContentView;
 
 private BoardDef.IPresenter mPresenter;
 
 private BoardViewProxy mViewProxy;
 
 @Before
 public void setUp() throws Exception {
 mPresenter = Mockito.mock(BoardDef.IPresenter.class);
 
 mContentView = Mockito.mock(View.class);
 Activity mockActivity = Mockito.mock(Activity.class);
 when(mockActivity.findViewById(android.R.id.content))
 .thenReturn(mContentView);
 
 mViewProxy = new BoardViewProxy(mockActivity);
 mViewProxy.setPresenter(mPresenter);
 }

Slide 56

Slide 56 text

Test the UI Android stuffs TextView ImageView … View Proxy Already Tested by framework team

Slide 57

Slide 57 text

Test the UI View Proxy Mock Mock Mock

Slide 58

Slide 58 text

Tests ViewProxy @Test
 public void testSetBoxValue() throws Exception { ViewGroup boxesViewGroup = Mockito.mock(ViewGroup.class); when(mContentView.findViewById(R.id.boxes_layout)).thenReturn(boxesViewGroup);
 ViewGroup firstRowViewGroup = Mockito.mock(ViewGroup.class); ViewGroup secondRowViewGroup = Mockito.mock(ViewGroup.class); ViewGroup thirdRowViewGroup = Mockito.mock(ViewGroup.class); when(boxesViewGroup.getChildAt(0)).thenReturn(firstRowViewGroup); when(boxesViewGroup.getChildAt(1)).thenReturn(secondRowViewGroup); when(boxesViewGroup.getChildAt(2)).thenReturn(thirdRowViewGroup);
 View firstRowFirstColView = Mockito.mock(View.class);
 View secondRowSecondColView = Mockito.mock(View.class);
 View thirdRowSecondColView = Mockito.mock(View.class);
 when(firstRowViewGroup.getChildAt(0)).thenReturn(firstRowFirstColView);
 when(secondRowViewGroup.getChildAt(1)).thenReturn(secondRowSecondColView);
 when(thirdRowViewGroup.getChildAt(1)).thenReturn(thirdRowSecondColView);
 mViewProxy.setBoxValue(0, 0, BoardDef.BOX_CROSS);
 mViewProxy.setBoxValue(1, 1, BoardDef.BOX_ROUND);
 mViewProxy.setBoxValue(1, 2, BoardDef.BOX_EMPTY);
 assertBackgroundColor(Color.RED, firstRowFirstColView);
 assertBackgroundColor(Color.BLUE, secondRowSecondColView);
 assertBackgroundColor(Color.GRAY, thirdRowSecondColView);
 }
 private void assertBackgroundColor(int expectedColor, View view) {
 ArgumentCaptor colorArg = ArgumentCaptor.forClass(Integer.class);
 verify(view).setBackgroundColor(colorArg.capture());
 assertEquals("The background expectedColor is not correct",
 expectedColor, (int) colorArg.getValue());
 }

Slide 59

Slide 59 text

Code Coverage Highlight part of code not tested The percentage is (quite) useless Display in Android Studio

Slide 60

Slide 60 text

Android Studio Execute app ↳ src ↳ main ↳ test ↳ java ↳ com.skocken.junittest ↳ ExampleUnitTest Right click > Run ‘All tests’ with Coverage (or Run ‘Junit’ with Coverage)

Slide 61

Slide 61 text

Singleton Goal: be able to mock it

Slide 62

Slide 62 text

Common Singleton patterns public class MyManager {
 
 private static class SingletonHolder {
 
 private static final MyManager INSTANCE = new MyManager();
 }
 
 private MyManager() {
 }
 
 public static MyManager getInstance() {
 return SingletonHolder.INSTANCE;
 }
 
 public void myMethod() {
 
 }
 } Testable? // or public enum MyManager {
 INSTANCE;
 
 public void myMethod() {
 }
 }

Slide 63

Slide 63 text

My testable Singleton pattern public class MyManager {
 
 private MyManager() {
 }
 
 public static MyManager getInstance() {
 return Singleton.getInstance(MyManager.class);
 }
 
 public void myMethod() {
 }
 }

Slide 64

Slide 64 text

Test public class GameBoard {
 
 ...
 
 public void myMethod() {
 MyManager.getInstance().myMethod();
 }
 
 } public class GameBoardTest extends TestCase { 
 ...
 
 @Test
 public void testShouldCallMockSingleton() { MyManager managerMock = Mockito.mock(MyManager.class); Singleton.setInstance(MyManager.class, managerMock);
 mSubject.myMethod(); verify(managerMock, times(1)).myMethod();
 }
 
 }

Slide 65

Slide 65 text

Credits @stan_kocken Presentation pattern: github.com/StanKocken/Presentation TicTacToe Example: github.com/StanKocken/TicTacToe Singleton: github.com/StanKocken/39d604bcf8c814a48859 Leak Canary: github.com/square/leakcanary Presentation pattern co-writer: Marc Olory