Single Page Web App UI Development Thoughts

I have been working on single page web apps’ UI for several years. Recently (actually it is about 1.5 years ago….), I accepted an offer from a giant company which would be definitely a huge monument in my life. So I would like to share some high level thoughts as well as design thinking on the development of single page web app UI based on my previous experience as my “Graduate Dissertation in Frontend”.

In this article, I will divide the content into 3 parts: ”UI Architecture Level Thoughts”, “Large Scale UI Design” and “UI Testing and Automation”.

1. UI Architecture Level Thoughts

1.1. Localization

Whenever you create a new web application, always implement localization first. Otherwise there would be tremendous refactoring cost and regression test cost when you want to add this feature to a developed web app. If you don’t want to spend much time to implement localization system at the beginning, you can define common localization APIs or unimplemented interfaces first and implement full feature later.

Localization contains the following aspects:
  • Language translation
  • Time zone conversion
  • Date, time, currency, number formatting and parsing

1.2. How to make code clean, extensible, low maintenance cost?

This is a common question in Computer Science. For here I’d like to limit my scope in frontend for this question. The answers are as follows:

1.2.1. Always try your best to build your UI element as a broadly reusable and configurable component.

Although in HTML and CSS, it is easier to implement an UI element with fixed size, fixed position and other fixed configurations, it’s always better to wrap an UI element in a component and make its size, position and other configurations dynamically depend on customized configuration or its parent’s size and position as much as possible. Because when your web app grows larger and larger, your PM will always ask you to reuse some components in other place and context. For example, assume one day you implement a sidebar in a static page. After release, this sidebar turns out to be a popular component among users. Then your PM may ask you to add you sidebar to some dialogs or to some nested elements. If you made all size, position and other configurations static during your implementation, you would need to refactor the code a lot and spend extra time to regression testing your sidebar so that the original feature wouldn’t be broken.

1.2.2. Make your component initializable to as many states as possible.

Although there are usually many configurations for one component, most components start with same state when they are inserted into DOM. For example, a table component’s initial state could only be in first page. A tree table’s initial state could only be a tree with all root nodes collapsed. Usually, there would be some extra logic in the conversion process from one state to another. If developer wants to initialize the state to other states like page 2 in list/table or some node expanded in tree table, he has to add extra logic to disable the state conversion logic which makes the code unnecessarily complicated. So it is always better to make your component initializable to as many states as possible to avoid those state conversion logic.

1.2.3. Use Object Oriented Design in Javascript

Although modern Javascript Version such as ES6, Typescript has added better OOD feature and many frameworks also have OOD features, my previous experience with Javascript was mostly non-OOD. OOD has some disadvantage. It is complicated and needs more design thinking which will result in longer development cycle. If your web application is very small and won’t grow large or your web app is mostly rendered in backend, then you don’t need to use OOD with Javascript. You can use simple Javascript and have fast iteration and short release cycle.

However if your application will grow very large and Javascript code is responsible for all UI logic and rendering such as single page app, then simple Javascript without OOD will make your code a mess and fragile.

For example, a table UI library could be used in different components and UX or PM wants to have some level of consistency across the whole project. Without OOD dev would simply create a common function to initialize the configuration of the table library which may contain both property settings and callback bindings. Since each component has its own requirement, dev would simply put all requirements in the common function and use id attribute to distinguish different components, then this function will become a mess. Another problem is when a dev want to add new features in this function, it is easy to break other features or some special and hidden scenarios and dev or QA also needs to spend a lot of time regression testing all related components because of even a simple change in such common function.

Imagine if we do this with OOD, the code would become cleaner, more extensible and easier to maintain and test. We could model different UI components in a hierarchical order, have a base function in the root class to initialize common configuration across all components and in each component level implement overridden function to add special configuration. If we want to add new feature to a subset of components, we can also achieve this with OOD patterns such as interface or mixins. We can implement simple unit test for each class. By doing all of the above, new feature or new configuration would usually only affect a small set of components and the stability would be guarded by those class level unit tests and developer could easily find what components his modification would affect by looking at the class hierarchy. I think that would reduce a lot of manual testing time and regression bug fixing time which may offset the extra development time for OOD.

Therefore I think OOD with Javascript for large and Javascript-heavy web application would make the code clean and extensible and reduce a lot of maintenance cost.

1.2.4. Make your code follow your business logic’s infrastructure and as data structure independent as possible

Even with OOD, there is still space to improve the reusability and reduce the redundancy. For example, suppose you want to extend a class and want to override a super class’s function. You have 2 options.

  1. Override function by copying the code and make modification on the copied code. 
  2. Call overridden function first and add extra code after it.
The latter option is obviously better but the problem is that not all logic can be added to the end or beginning of the old logic. Some logic may need to be inserted to the middle of the old logic. Some logic may need to swap 2 lines in old logic. Sometime to change a function’s input parameters’ the data structure, developer has to completely change the old code’s structure. For example, the old input parameter could be an array while the new input would be a map. So in the above cases, we have to use option 1. However, it makes code redundant. And redundant code not only looks bad but also makes developer easy to forget to synchronize the code change in one place to multiple redundant places. So even with option 1 we should also try to reduce redundancy as much as possible.

To address this problem, I think developer should follow your own business logics’ infrastructure by clearly defining your business logic and having good mapping from these logic to code.

All codes are used to implement your business logic. I think bad reusability is always caused by bad mapping from code function to business logic. Sometime we mixed bunches of business logic related code in one big function. And later, to implement a new feature, a different data structure may be needed for the same business logic or only first part and third part of the business logic may be needed in which case we have to either refactor the old code or copy the old code. Sometime when we define functions, we only considered the immediate needs and tried to find a most concise implementation while ignoring the future needs.  And later similar business logic may also be needed but there is no easy way to reuse the previous highly optimized code. Sometime in order to use some tricks in a language or a library, we put part of business logic in one function and the others in another function.  And later the whole business logic may be needed to be implemented in a different way.

To avoid the above mistakes we should go through 2 steps.
  1. We should clearly define and componentize business logic. We should create an acyclic graph to represent the whole business logic. Each vertex is a component representing a business logic which could be described in short words. Each edge is a relationship. For example, business logic A could be composed of business logic B, C and D and A could be used in E and F. We should try our best to divide one business logic into as many business logic as possible to increase the reuse possibility.
  2. We should define a function in code for every business logic. We should make the function as data structure independent as possible by using object as input instead of any particular data structure because in the future we may want to implement the same logic with different data structure of input. If a particular data structure is really needed to be the input, we can create a wrapper function whose input is a data structure and will call the business logic function inside it. 

1.2.5. Use same design pattern across whole project

Usually a framework such as Angulerjs and Ember provides so many features that developer can achieve same goal with different patterns and ways. However it would add extra complexity and instability when developer wants to integrate several components which are developed in different patterns by different developers into a new large component and create high dependency among them.

So during the infrastructure design stage of a project, it is better to set common design patterns and solutions for some common questions and problems.

1.2.6. All functions should use a single object as input parameter.

Unlike Java, which looks up function by both functions name and parameters, Javascript looks up function only by function name. When calling a function, developer could put arbitrary number of input parameters and the remaining would be set as undefined. This feature makes it easy for developer to change a function’s signature but it also makes the code mussy.

Consider the following scenario:
  1. A function is defined as func( a, b);
  2. A developer adds a few new parameters into this function and it becomes : func(a, b, c, d, e);
  3. Another developer who is working on another feature wants to add one more parameter to that function and change usage of it in one place to use that parameter. Then the function signature becomes: func(a , b , c , d , e , f); The function call becomes: func( va, vb, undefined, undefined, undefined, vf);
As you can see, in order to use one additional parameter, we need to add 3 “undefined” in the call which I think doesn’t look clean especially when your function parameter set is very large.

To solve this problem, we can use a single object as function parameter and add input parameter as the object’s value.

For example, the previous function’s definition could be rewrite as func( paramObj ) in the comment we could specify the paramObj definition like:

paramObj = {
    a: //…..,
b: //…..,
c: //…..,
d: //…..,
e: //…..,
f: //…..
}

Then for the previous modified function call, we can change it to:

func({
a: va,
b: vb,
f: vf
});

As you can see after the change, it looks much cleaner.

1.2.7. User action handler should only be triggered by user action.

This statement may seem redundant. But sometimes user action handler is reused in function call or user action could be triggered by function call. I think it is not good to do that because user action handler may contain some logic only for that particular user action or it may be added new logic only for that action.  These logics should be disabled if the handler is called from function which seems to be unnecessary complexity. So when a developer codes an action handler, he should divide and wrap different stuffs in separate function to be reused. And other developers shouldn’t directly reuse the handler. Instead they should reuse the exact function they need in the handler or they should refactor the handler code and wrap what they need in a separate function and then reuse it.

1.2.8. Integrate Javascript UI lib with Javascript framework lib

Many Javascript UI libraries are written in pure Javascript and directly use DOM API or JQuery API to create HTML components. However, most Javascript framework libraries use template language to generate HTML components. The discrepancy also exists in design pattern. For example some library uses observer while others use event handler.  This causes problems and challenges when you want to use a Javascript UI lib with your Javascript framework lib. Because a bad integration could make your code more error prone, less maintainable and less extensible.

The solution is to create a wrapper class with your Javascript framework lib on the Javascript UI lib which converts all the Javascript UI lib’s patterns to your Javascript framework’s patterns. All other code in your app should only call the wrapper class’s API and should not access the Javascript UI lib’s API directly. You can put all Javascript UI lib generated HTML in a ShadowRoot to keep the markup structure, style, and behavior hidden and separate from other code on the page so that Javascript UI lib code doesn’t clash with your own code.

To integrate your Javascript UI lib generated HTML with your template language, there are 2 options:
  1. Some Javascript UI lib could style your existing HTML code to make it looks as expected. They don’t create new HTML node. With such feature, you can render your template language into HTML first and then style the HTML.
  2. Or you can create a placeholder in your template for Javascript UI lib first and then render your UI component into the placeholder.

1.3. Single page app Architectural Pattern

1.3.1. Never use observer

Clarification: Observer here refers to the observer in old Ember version (Not sure if it is still there 😋). It doesn’t contain event listener. In old Ember version, observer is a function triggered when a value changed. For example, developer can define a function like:
function () { …}.observe(“a.value”).

Observer is a common pattern in large web app and many frameworks support this pattern. I have also used this pattern a lot in my previous work but I think it’s a bad pattern. People may say observer makes data-binding easier. It’s true if your object model is well defined and no more development on it.

Suppose value B depends on value A. Value A may be changed in many different places. If we use observer pattern, we only need to define an observer handler on B instead of adding code to fire change B event in every place value A is changed. That’s easier. However, what if in the future you want to change value A’s updating logic? For example, suppose currently there are 2 places where it will change value A to same value. Let’s say they are place C and place D. In a new task, you don’t want to change B’s value if value A is changed in place C or you may want to change B to a different value if value A is changed in place C. Then you would need to add some special flag to the context which would usually be a complicated and ugly implementation so that B’s observer could read that flag and know the change is from place C. But if you directly call change value B function from place C and D instead of using observer, you only need to add one more input parameter to the change function which is easy in Javascript.

Another example is that suppose when you were implementing B’s observer, you didn’t know every place which changed value A. And based on your imperfect knowledge of A’s modification, you decided B’s observer logic and implemented it. Then later bugs will be fired for bad behavior when some uncovered places modify A’s value. Those uncovered places may even be out of your task’s requirement. But if you don’t use observer, you only need to implement the B and A’s dependency logic based on what you know and the task requirement. For those uncovered places, it either won’t cause any bad behavior in your task related to B or is not necessary to have the dependency. People may say observer is good where it renders data directly to view because there wouldn’t be complicated logic in rendering. But this is usually supported by HTML template in most frameworks.

So I strongly recommend use as few observers as possible.

1.3.2. Event dispatcher - listener pattern is not recommended.

The event here is only developer defined event. It doesn’t contain any browser event like (‘click’, ‘focus’ …). Browser event listener is always fine to use.  Comparing to the observer pattern mentioned above, event listener pattern is more flexible. However, I still don’t recommend using it. I think event dispatcher pattern is mainly used in library and platform development because library/platform developer doesn't know how application developer want to handle events and they want to give application developer more freedom in communication with platform/library.  For example, web browser dispatches event through Javascript code so that Javascript application could handle an event at any level of container and with any handlers.

However, within an application, there are cons:
  1. There could be many subtle UI state changes which may trigger only one other change. If we define one event for each subtle state change, there would be too many useless events to manage.
  2. If an event’s meaning isn’t clearly defined, it may be abused by developer who is not familiar with its dispatching logic and then cause some bug at some special case.
Since Web app’s UI is usually developed by a single team, the team should completely know what logic should be triggered by a UI state change.  Therefore the main pros for event dispatcher – listener pattern doesn’t make sense here. That’s why I don’t recommend this pattern.

1.3.3. Framework Pattern

There are many UI architectural patterns such as Model – View – Controller and Model – View – ViewModel. In each pattern, the terms “model” and “view” have slight different meanings and responsibility.

In my following discussion, I will use Model – View pattern.
  • Model is classes which communicate with backend, fetch data from backend and manage data model objects in frontend.
  • View is classes which render HTML templates and handle user actions. 

As the UI grows larger, the logic and code become very complicated which needs to apply good design pattern to make the code high readable, easy to maintain and stable. The complexities are mainly in 2 areas:
  1. One user action could trigger a long and complicated chain of UI states and data update.
  2. Sample chain:

  3. The dependencies of code could be very complicated like a skein. Views could depend on each other. One view could depend on its parent, child, sibling and even distant relatives. Data models could also depend on each other. And one view could depend on multiple data models. One model could be depended by multiple views. Multiple views could depend on same data model.
The above complexities could cause following problems:
  1. Since backend calls are usually asynchronous, a long serial chain of UI states change, backend calls and UI rendering could exponentially increase the number of scenario to be handled by code.
  2. Same UI state, rendered UI and data model could be updated multiple times in a single chain triggered by one user action.
  3. Sometime in order to call distant relative’s function, developer has to use tricky or error prone way to obtain a reference of the distant relative object.

To address above problems, we should:
  1. Organize and group the code into 3 phases:

    Phases:
    1. Update data model with backend API:  load data from backend. The data should contain any data MAY BE used after a user action.
    2. Update UI states:  Javascript code update all related UI states with data from the model
    3. Render UI into HTML: Call any UI rendering callback. It shouldn’t request any new data to be loaded. All data should have been loaded in phase 1.

    To convert the serial chain in Figure 1 to Figure 2’s pattern, we should postpone and execute in advance all same type of changes to its phases. For example, in Figure 1, some data model change happens before a UI state update and the other data model change happens after UI state change. After conversion, we should first fetch and update all data model which could be triggered by a user action and then update all UI states together. Some data model may only need to be updated or fetched from backend in some particular condition depending on UI states. In the 3 phases pattern, after a user action happens, we should predict what data is needed and always update and fetch these datas. This deserves the effort and overhead because it reduces the complexity.

  2. Decrease number of dependencies among views. Use view –> model -> model -> view pattern instead. For example, instead of letting View A directly change View B’s state, let View A change model A, then Model A changes Model B, finally Model B changes View B. The advantages of this approach are
    1. Data model usually has simpler structure than views. So it is easier and less tricky to find a reference of other data model and call other data model’s function in one data model.
    2. A view can be reused without adding complicated branching logic to call different other views’ function in different conditions.
Besides the major UI architectural problems discussed above, there are 2 minor problems:
  1. Too many “IF” statements in common functions. Sometime we extract some common logic into a common function. Then as we add more and more components, we have to add many special logic wrapped in “IF” block into the common function which is less readable.
  2. We want to isolate a component from others so that it can be reused at different places while sometime we also want to store other components’ references  in current components.
The common solution to them is Object Oriented Design. With OOD, we can put common logic in a basic function and extend it for special component to add special logic. We can create a base class for a component which is most reusable. In a special application context, we can extend the base class to contain other components’ references in that context.

1.4. Debugging:

Sometime when a bug happens, it is hard to figure out the root cause because the app is very large and complicated. To make debugging easier and avoid manually adding too much console.log code, we should develop some kind of Javascript annotation (like “@debug”) which tells compiler to add a console.log statement below the next statement containing next statement’s necessary information like function return, property values and so on.

1.5. Analytics:

There are 2 common metrics: impression and user interaction. Sometime impression is hard to collect because it is hard to determine if an element’s impression happens. The impression could happen when the element is inserted into DOM or when the element’s “display” CSS attribute is toggled or when the element is scrolled into screen or when the element’s “z-index” is increased. So there are many scenarios could trigger an element to be impressed.

Thus to make it easy for analytics infrastructure developer to log the impression, each component should implement an analytics interface which contains functions indicating if a component is impressed.

2. Large scale UI design

Here large scale means data rich. In this part I will discuss some common patterns used in large scale UI.

2.1. Asynchronous call

Javascript asynchronous call could cause many complexities:
  1. The number of cases is exponential to number of Promise. When you have multiple Promise, you need to consider the cases that some of them succeeds, and some of them fails which is exponential.
  2. Multiple Promise could also cause race condition.
  3. Zombie Promise could cause problem. Consider the following scenario:
    1. Component A creates Promise A to asynchronously fetch some data.
    2. Component A is destroyed before Promise A is resolved. At this point, Promise A becomes a zombie. It could still be resolved when the asynchronous call returns.
    3. Then a new component A is created and a new Promise A is created.
    4. If the old zombie Promise A is resolved after the new Promise A is created or resolved, it could create many potential errors.
  4. Zombie Promise could also cause memory leaks if its handler contains some large object references.
Since adding asynchronous call into a synchronous function could cause a lot of issues, we should either:
  1. Restrict the usage of asynchronous call to very limited cases:
    1. A component’s UI can be separately updated by the asynchronous call without affecting other components to minimize the Promise’s impact.
    2. Some background data related tasks.
    We should cache all promises so that they can be canceled on component disposal to avoid zombie Promise problem.

    We could convert library API’s asynchronous return to synchronous syntax by using “await / async” call.

    By limiting the usage of asynchronous call, we could reduce the above complexities as much as possible.

  2. Use Promise as function return type as much as possible at least in your main infrastructure function’s return type. And build general infrastructure to handle all Promise’s rejection and cancelation.

    This pattern could provide an open door to implement some framework level solution for exponential cases, race condition and zombie Promise which would make component developer’s life easier when they use Promise.

2.2. Pagination

Pagination means dividing data into page and adding page navigator buttons to your table or list.

Every type of data could grow to a very large amount as time elapses including even some pre-defined constant data. So every data list or data table UI should use pagination by DEFAULT. If you don’t want to use pagination, you should provide strong justification to that.

2.3. List and table UI and their CRUD operation

List and table are frequently used UI to display a set of data of same type especial when the data size is very large. CRUD stands for Create, Read, Update, and Delete.

List/table UI usually has following features:
  1. Pagination
  2. Sort by different fields
  3. Search or filter by data’s fields’ values
  4. Create, update and delete rows.
One big challenge is list/table’s load, search and refresh performance especially when data size is very large.
  1. In backend, cache the list data to improve the read, sort and search performance. The cache should only contain the columns displayed in the list. It shouldn’t contain the other columns of each data which are not displayed in the list/table so that it could reduce memory usage of the cache. Since this is only a frontend technique article, I won’t discuss the backend cache design but I do think it is doable as long as not too many columns are needed to be shown in the list/table.
  2. In the list/table, only show minimum number of columns of a data which are usually logical keys of that data to reduce load and complexity of backend cache.
  3. If user wants to see more detail of a data, he could click that data to see a detail view. Note that it is better to hide the list at the same time to reduce complexity.
  4. Backend could also maintain a LRU cache to store some data’s full detail.

The CRUD operations add another layer of complexity on table/list UI. To simplify that we should:
  1. When user edits detail, hide or disable as many buttons and clickable items as possible to reduce complexity and prevent wrong user actions.
  2. After user creates a new record, to assure user the new record is inserted into the list, we can keep the current search filter and sort order and navigate to the page containing the new record if the new record is not filtered out by the search filter.  You can also change the current filter or sort order to show only the new record or show the new record on top of the list. But that would change user’s original filter or sorting order. If user wants the previous filter and sorting order, he has to redo the search or sorting which is extra trouble for the user. With a configurable component mentioned earlier in this article, developer could easily refresh the list to show the page containing the new record.

2.4. Data relationship

In data rich application, sometime the data model is very complicated. Tens of types of data could have relationship with each other. The relationship is either a reference field pointing to other data or a relationship table consisting of multiple types’ logical keys. So the challenge is how to make the UI easy for users to navigate through the relationship, research the relationship and view multiple related data at the same time.

We can’t present all relationships corresponding to one piece of data on same page because
  1. There could be too many related data to present on one page.
  2. It makes user hard to find and focus on the particular relationship he really cares.

Therefore, for each data type and its relationships, we should figure out the most important relationships user cares first and then only present these relationships on same page. For other relationships, we can put a reference link in the detail view to allow user click it and view it in other window or page.

The following UI features could help better show data relationship:
  1. For each data object, only show its minimum identifiers on the page and show its detail in a separate window or pop-up window. To make it easy for user to identify individual data object, we could display minimum identifiers on the table or list. User could click the data to see the full detail of it in a pop-up window or separate window. In this way, we can display more data and related data in one page and not make it look too crowded.
  2. Show data relationship in graph. This is only suitable for the relationship graph which has moderate depth and breadth.  If a data has 1 million related data, we should show these data in a separate table with pagination instead. Because there would be no space to show the whole graph.
  3. For less important relationship or large data relationship, show them in separate window or table or list. The following features could make user move around related data objects across tables and windows quicker and easier.
    1. Breadcrumbs links or quick navigation links;
    2. Forward and backward buttons;

2.5. Tree UI

Tree UI is a special type of data relationship which is both simple and frequently used in different applications.

There are 2 main types of tree UI:
  1. Show whole tree in one table with collapse and fold structure
      • Pros:
        • Clear tree structure
        • Single table, simple and straight forward, save space
        • Easy to support a single search on all tree nodes
      • Cons:
        • Hard to do pagination
        • No good way to show 1 million children under same parent
        • Make UI very complicated if child and parent have different columns
    UI example
  2. A list of list. Each list represents one level of nodes and all lists are placed in vertical order. Inside each list, tree nodes of same level and same parent are placed in horizontal order.
      • Pros:
        • Easy to support pagination on each level so that we can divide 1 million children into pages
        • Easy to support search filter on each level
        • Parent and child could have different columns
      • Cons:
        • Take too much vertical space especially when we want to show multiple columns in each list
        • Hard to support a single search on all tree nodes
    UI example



2.6. Concurrency and Consistency

Concurrency and consistency are 2 big concerns in data rich UI.

Concurrency could happen when user do CRUD operations. 2 Users could update same record at the same time or shortly nearby. 2 Users could also create new records with same identifier at the same time or shortly nearby. One user could update one record and another user could delete that record at the same time or shortly nearby.

To avoid confusion and conflicts when concurrency happens, we need to provide some level of consistency.

But concurrency and consistency are hard to guarantee at the same time. So we need to evaluate different requirements and do trade-off.

Requirements:
Type 1: Consistency is most important, low chance of concurrent edit and low amount of concurrency on same record. This could happen on application which is used by business customers.

Solutions:
  • Lock the data being edited. When one user wants to edit one data, he needs to lock it first so that other user cannot edit the data at the same time. Since in this requirement type, there are low amount of concurrency on same record, the lock shouldn’t be too expensive.
  • Push data updates to other users. Use WebSocket’s message event to push updated data from server to all clients displaying that data. It might be hard to track exactly which clients are displaying the data. But server doesn’t need to know which clients are displaying the data. It could just push the updates to all clients which are active and requested the data before. When a client receives the update event, it could either ignore or handle it.  The update events shouldn’t take too much server bandwidth since we assume low concurrency on same record.


Type 2:  High chance and amount of concurrency, consistency is less important. This could happen on application which is used by individual customers.

Solutions:
  • Client polls the latest records. Since concurrent client amount is large, maintaining a list of active clients for each record on server could be very expensive. So instead of server pushing updates to clients, we should make client poll the latest records. And on server side, we could use multiple servers to serve polling request and do load balance.
  • Servers store multiple copies of same records.  Store multiple copies of same records on multiple servers to minimize concurrent request latency.
  • No resource lock. Because it is expensive with multiple servers and high concurrency.

3. UI Automation Test

Automation test is very important for UI because there are too many test cases to be covered by human testers. The number of test cases is exponential to number of clickable components in UI.

The easiest way to do UI automation test is deploying all your frontend components and backend components to a separate environment and writing scripts to test the whole application. But this way usually produces flaky test results.

The flakiness is usually caused by timing uncertainty in network, browser, database, server and operating system. In our test code, we usually wait for something to happen to verify the test result. The test framework could also fail the test if the test doesn’t finish after some timeout value. The timeout failure could either be expected or unexpected. It could be expected when we design it to fail if nothing happens after some input. It could be unexpected because of timing uncertainty.  So timing uncertainty could cause false negative(flaky) test result and a lot of trouble for developer to develop automation tests.

To minimize the flakiness and timing uncertainty, we could
  1. Test each component separately. When testing one component, we could create a simple mock of other components. For example, when testing only frontend, we could mock backend APIs and data.
  2. Use mock platform in integration test containing multiple components. If we can find some mock platforms such as mock browser or mock operating system which allows your test framework to control its timing, it would greatly reduce integration test’s flakiness.

    For browser, we can override setTimeout() and setInterval() functions in Javascript to control the timing. But we cannot control CSS animation’s timing which makes it hard to verify CSS animation in an easy way.
Full integration test with real platforms is also useful but it should only be used as a system stability metric and should not waste too much developer’s time to figure out why it fails or why it is flaky.

Popular posts from this blog

View and Build Government Organization like a Software

Does Free Consciousness exist ?

Software Architecture Books Summary And Highlights -- Part 1 Goal, Introduction And Index