Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testowalny kod na Androida? Spróbujmy z Clean A...

Testowalny kod na Androida? Spróbujmy z Clean Architecutre!

Talk given o 4developers.pl in 2015.

Avatar for Michał Charmas

Michał Charmas

April 20, 2015

More Decks by Michał Charmas

Other Decks in Technology

Transcript

  1. O mnie • Developer aplikacji mobilnych na Androida • Trener

    w firmie Bottega IT Solutions • Konsultant / Freelancer
  2. • Kwestie techniczne / optymalizacyjne - raczej łatwe i dobrze

    opisane • Jak żyć? • jak bezboleśnie wprowadzać zmiany w aplikacji? • jak nie psuć wcześniej działających ficzerów wprowadzonymi zmianami? • jak testować? • jak dzielić odpowiedzialność? • jak osiągnąć mniejszy coupling? • jakich patternów / biliotek używac żeby to osiągnąć
  3. –Android Developers Blog „…the other primary goal is to serve

    as a practical example of best practices for Android app design and development.”
  4. • Gdzie zasada pojedynczej odpowiedzialności? • Logika domenowa w UI

    • Logika UI pomieszana z asynchronicznym pobieraniem danych • Callbacks everywhere • Mapowanie kursorów na obiekty biznesowe w UI • Activity i Fragmenty po 1000+ linii • Całkowite uzależnienie od frameworka (import android.*) • Testy?
  5. A 2 B 4 A 2 B 4 A 2

    B 4 2 * 4 = 8 2 + 4 = 6 * * J.B. Rainsberger - Integrated Tests Are A Scam (https://vimeo.com/80533536)
  6. Application Networking Persistance Android SDK Models / Domain Android Application

    Application Android SDK Networking Persistance Models / Domain import  android.* „szczegół techniczny”
  7. • Niezależna od frameworka. • Niezależna od interfejsu użytkownika. •

    Niezależna od bazy danych. • Testowalna - w oderwaniu od frameworka / bazy danych / serwera.
  8. public class Product {
 private final long id;
 private final

    String name;
 private boolean isBought;
 
 public Product(long id, String name, boolean isBought){
 this.id = id;
 this.name = name;
 this.isBought = isBought;
 } 
 public void markBought() {
 this.isBought = true;
 }
 
 public void markNotBought() {
 this.isBought = false;
 } //getters
 } @Test public void testShouldBeBoughtWhenMarkedAsBought() throws Exception {
 Product product = new Product(0, "sample name", false);
 product.markBought();
 assertEquals(true, product.isBought());
 }
  9. public class ProductList implements Iterable<Product> {
 
 public Product addProduct(String

    productName) {
 }
 
 public int removeBoughtProducts() {
 }
 
 public Product getProduct(long id) {
 }
 
 public int size() {
 }
 
 @Override public Iterator<Product> iterator() {
 }
 }
  10. • UseCases: • AddProduct • ListProducts • ChangeProductBoughtStatus • RemoveAllBoughtProducts

    public  interface  UseCase<Result,  Argument>  {      Result  execute(Argument  arg)  throws  Exception;   }   public  interface  UseCaseArgumentless<Result>  {      Result  execute()  throws  Exception;   }  
  11. public  class  AddProductUseCaseTest  {      private  AddProductUseCase  useCase;  

       @Before  public  void  setUp()  throws  Exception  {          useCase  =  new  AddProductUseCase();      }      @Test  public  void  testShouldAddProduct()  throws  Exception  {          useCase.execute("Sample  product");          //TODO:  verify  saved      }   }  
  12. public interface ProductsDataSource {
 ProductList getProductList();
 void saveProductList(ProductList products);
 }


    public class AddProductUseCaseTest {
 private static final String PRODUCT_NAME = "Sample product";
 @Mock ProductsDataSource productsDataSourceMock;
 @Mock ProductList mockProducts;
 
 private AddProductUseCase useCase;
 
 @Before public void setUp() throws Exception {
 MockitoAnnotations.initMocks(this);
 useCase = new AddProductUseCase(productsDataSourceMock);
 }
 
 @Test public void testShouldAddProduct() throws Exception {
 when(productsDataSourceMock.getProductList()).thenReturn(mockProducts);
 
 useCase.execute(PRODUCT_NAME);
 
 verify(mockProducts, times(1)).addProduct(PRODUCT_NAME);
 verify(productsDataSourceMock).saveProductList(mockProducts);
 }
 }
  13. public class AddProductUseCase implements UseCase<Product, String> {
 private final ProductsDataSource

    productsDataSource;
 
 @Inject
 public AddProductUseCase(ProductsDataSource productsDataSource) {
 this.productsDataSource = productsDataSource;
 }
 
 @Override
 public Product execute(final String productName) {
 if (productName == null || productName.trim().isEmpty()) {
 throw new ValidationException("Product name cannot be empty");
 }
 ProductList productList = productsDataSource.getProductList();
 Product product = productList.addProduct(productName);
 productsDataSource.saveProductList(productList);
 return product;
 }
 }
  14. • całkowicie niezależne od frameworka • pure Java • może

    zostać wyciągnięte do oddzielnego modułu - czysto javowego
  15. • Domena nie wie o sposobie utrwalania danych. • Sposób

    utrwalania danych nie powinien operować na obiektach domeny (wyciekająca abstrakcja od środka). • Wyznaczenie granicy. • Trzymanie się „zasady zależności”.
  16. public  class  ProductEntity  {      private  final  long  id;

         private  final  String  name;      private  final  boolean  isBought;      public  ProductEntity(long  id,  String  name,  boolean  isBought)  {          this.id  =  id;          this.name  =  name;          this.isBought  =  isBought;      }      public  long  getId()  {          return  id;      }      public  String  getName()  {          return  name;      }      public  boolean  isBought()  {          return  isBought;      }   }  
  17. • ProductDataSourceImpl - implementacja oderwana od zewnętrznych bibliotek • Zależy

    tylko od ProductEntityStore i Mappera (Product -> ProductEntity) • Łatwe do zmockowania aby przetestować w izolacji. public interface ProductEntityStore {
 List<ProductEntity> getAllProduct();
 void storeAllProducts(List<ProductEntity> entities);
 }
  18. public  class  SharedPreferencesProductEntityStore  implements  ProductEntityStore  {
    private  final  SharedPreferences

     sharedPreferences;
    private  final  EntityJsonMapper  entityJsonMapper;
    //…   
    @Inject
    public  SharedPreferencesProductEntityStore(SharedPreferences  sharedPreferences,
            EntityJsonMapper  entityJsonMapper)  {
        this.sharedPreferences  =  sharedPreferences;
        this.entityJsonMapper  =  entityJsonMapper;
    }
 
    @Override  public  List<ProductEntity>  getAllProduct()  {
        try  {
            return  entityJsonMapper.fromJson(                      sharedPreferences.getString(SP_PRODUCT_ENTITIES,                        „[]”              ));
        }  catch  (JSONException  e)  {
            handleJSONError(e);
            return  Collections.emptyList();
        }
    }   //…   
 }  
  19. public  class  SharedPreferencesProductEntityStoreTest  extends  AndroidTestCase  {
    private  SharedPreferencesProductEntityStore  store;


    
    @Override  public  void  setUp()  throws  Exception  {
        super.setUp();
        store  =  new  SharedPreferencesProductEntityStore(
                getAndClearSharedPreferences(),
                new  SharedPreferencesProductEntityStore.EntityJsonMapper()
        );
    }
 
    public  void  testShouldStoreEmptyList()  throws  Exception  {
        store.storeAllProducts(Collections.<ProductEntity>emptyList());
        assertEquals(store.getAllProduct().size(),  0);
    }
 }
  20. UI

  21. public interface UICommand<T> {
 void execute(T ui);
 } private Queue<UICommand<T>>

    commandQueue = new LinkedList<>(); UI1 P1 UI2 P1 AsyncOperation P1
  22. @Singleton
 public class ProductListPresenter extends BasePresenter<ProductListPresenter.ProductListUI> {
 
 @Override protected

    void onFirstUIAttachment() {
 updateProductList(true);
 }
 
 public void productStatusChanged(long productId, boolean isBought) {
 }
 
 public void onAddNewProduct() {
 }
 
 public void onRemoveBoughtProducts() {
 }
 
 private void updateProductList(boolean withProgress) {
 if (withProgress) {
 execute(new ShowProgressCommand(), true);
 }
 asyncUseCase.wrap(listProductsUseCase).subscribe(
 new Action1<ProductList>() {
 @Override public void call(ProductList products) {
 List<ProductViewModel> viewModels = mapper.toViewModel(products);
 executeRepeat(new PresentContentCommand(viewModels));
 }
 }
 );
 }
 }
  23. @Singleton
 public class AddProductPresenter extends BasePresenter<AddProductPresenter.AddProductUI> {
 private final AddProductUseCase

    addProductUseCase;
 private final AsyncUseCase asyncUseCase;
 
 @Inject public AddProductPresenter(AddProductUseCase addProductUseCase,
 AsyncUseCase asyncUseCase) {
 this.addProductUseCase = addProductUseCase;
 this.asyncUseCase = asyncUseCase;
 }
 
 public void onProductNameComplete(final String productName) {
 asyncUseCase.wrap(addProductUseCase, productName)
 .subscribe(
 new Action1<Product>() {
 @Override public void call(Product product) {
 executeRepeat(new NavigateBackCommand());
 }
 },
 new Action1<Throwable>() {
 @Override public void call(Throwable throwable) {
 if (throwable instanceof ValidationException) {
 executeOnce(new ShowValidationErrorCommand());
 }
 // TODO: handle unknown error
 }
 });
 } }
  24. private static class ShowValidationErrorCommand implements UICommand<AddProductUI> {
 @Override public void

    execute(AddProductUI ui) {
 ui.showValidationError();
 }
 }
 
 private static class NavigateBackCommand implements UICommand<AddProductUI> {
 @Override public void execute(AddProductUI ui) {
 ui.navigateBack();
 }
 }
 
 public interface AddProductUI extends UI {
 void navigateBack();
 
 void showValidationError();
 }
  25. public class AddProductFragment
 extends PresenterFragment<AddProductUI>
 implements AddProductUI {
 
 @Inject

    AddProductPresenter presenter;
 private EditText productNameView;
 
 @Override public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setupPresenter(presenter, this);
 }
 
 @Override public View onCreateView(LayoutInflater inflater,
 ViewGroup container, Bundle savedInstanceState) {
 return inflater.inflate(R.layout.fragment_add_product, container, false);
 }
 
 @Override public void onViewCreated(View view, Bundle savedInstanceState) {
 super.onViewCreated(view, savedInstanceState);
 productNameView = (EditText) view.findViewById(R.id.product_name_view);
 view.findViewById(R.id.product_add_button)
 .setOnClickListener(new View.OnClickListener() {
 @Override public void onClick(View v) {
 presenter.onProductNameComplete(productNameView.getText().toString());
 }
 });
 }
 
 @Override public void navigateBack() {
 ((ProductListFragmentActivity) getActivity()).onProductAdded();
 }
 
 @Override public void showValidationError() {
 Toast.makeText(getActivity(), "Product name cannot be empty.", Toast.LENGTH_SHORT).show();
 } }
  26. Zapisywanie stanu • Input użytkownika jest stanem - nie model

    danych. • Stan == Parcelable • UI Zapisuje swój własny stan - EditText, ListView z automatu. • Powinniśmy trzymać stan w Presenterach?
  27. DI

  28. Kontener DI • ma konstruktor @Inject - wiem jak go

    znaleźć - tworzę i zwracam • nie wiem skąd go wziąć? - sięgam do modułu Moduły Litania metod dostarczających zależności. Dagger daj obiekt ObjectGraph @Singleton
  29. AppObjectGraph • AndroidModule • DataModule • DomainModule Application Scope PresenterActivityOG

    • PresenterModule • Preseneter - @Singleton onRetainCustomNonConfigurationInstance ActivityOG • ActivityModule Activity Scope
  30. Wady / Zalety? • Overhead dla małych aplikacji - na

    pewno wiadomo, że nie urośnie? • Duża ilość wywołań metod. • Mniej optymalne rozwiązanie pamięciowo - tworzy i zwalnia się dużo obiektów, fragmentacja pamięci. • Samo podejście i ,,zasada zależności” słuszne - umożliwiają testowanie.