DeviQA

DeviQA - is an independent software testing and quality assurance company.

Software Testing of Electron Based Application

So I’m sitting around and waiting for a challenge, dreaming of having a project where I won’t see all these boring aspects like Webdriver, BrowserStack, Web pages, etc. And then…
Finally, it appeared. It was a desktop application for Mac based on the Electron framework. If you are not aware — the Electron application is a browser wrapped inside Electron elements and API. I was a bit upset because it seemed very easy. However, after starting the development, I was surprised…

What is Electron and how can I work with it?

First of all, I want to describe what the Electron is, and how to use it. Also, I’d like to add a few words about the approaches on how to automate these types of applications.
Electron is a framework developed by GitHub. This framework is based on JavaScript, which means it can be easily used for different platforms (Windows, Mac, Linux OSs). The framework uses NodeJS as the backend and Chromium as the frontend. Through using these technologies, it becomes possible to create a native application using HTML, CSS, and JavaScript, and this approach is the right solution for building native apps with such Web technologies.

What test framework should I choose for automation and why?

So, the question is what test framework to choose to automate such apps. On the one hand, we can use general frameworks for web testing, and, on the other, we have Spectron.
If we go with the general frameworks, we won’t be able to use internal methods to have access to the Electron app, such as app, dialog, menu, clipboard, and so on.
Therefore, it definitely should be Spectron.
It is built upon WebdriverIO (WDIO). WDIO is an implementation for selenium’s W3C Webdriver API that allows communication with the browser, in our case, with the Electron application. Moreover, Spectron supports full Electron APIs, which is a valuable benefit.
My journey started with creating a folder named ‘SpectronTestSuite

JavaScript? No! Typescript.

I understand that I should choose JavaScript as a programming language for automated test development, but I actually prefer to use TypeScript.
TypeScript has static typing that can help me to avoid mistakes with types and arguments transferred to my methods. This advantage makes the code clean with full transparency and understanding of what the current piece of code is doing. Moreover, it will help me during the refactoring.
One of the most significant benefits of TypeScript is the ability to compile the code to ES5, which can be easily understood by our browser/application. Sometimes new features are not supported by old browsers, but we can leave this hard work to our compiler without requiring any interventions.

Test Structure Development

Following this, I created a typical structure; pages to page folder, specs to spec folder, etc. It’s fairly common to set up an automation test suite and thus I won’t be paying attention to it in this article. I want to show some insights that can help you to avoid unpredictable issues in developing automated tests for Electron apps.

Creating a config file

I suggest to create a config file (in my case it is config.ts) with all the needed parameters for your application. In my case, it looks like this: 
export default {
   userOne: {
       name: users.firstUser.username,
       startParameters: ['--require', '../src/test/core/mock-dialog/preload.js', './dist/main.js'],
       chromeDriverPort: '9515',
   },
   userTwo: {
       name: users.secondUser.username,
       startParameters: ['--require', '../src/test/core/mock-dialog/preload.js', './dist/main.js', '--second'],
       chromeDriverPort: '9516',
   },

   testFilesStorage: 'src/test/test_files/',
   testDataFileName: 'TestData.csv',
   testDataFilePath: 'src/test/test_data/',
   defaultWaitTime: 30000,
   waitTimeForElementExist: 10000,
   waitBetweenStates: 1000,
}

Add the constructor

Now add the following constructor to your wrapper file that contains all core methods like click, waitForElement, etc. My constructor has the following parameters. The `path` attribute — to run the application via dist (this will be covered in the next part of the article) and the `args` — to set up/run configuration for the app. In my case it’s path to `main.js` file, as well as some of the required files needed before the application start. The third parameter is a port for ChromeDriver.
app: Application;
user: any;
dialog: any;
name: string;
fs: any;

constructor(user) {
    this.user = user;
    this.name = user.name;

    this.app = new Application({
        path: path.join('./', './node_modules', '.bin', 'electron'),
        args: this.user.startParameters,
        port: this.user.chromeDriverPort
    });
    this.dialog = fakeDialog(this.app);
}

Adding a new instance

Please note that you need to add new instance creation (in my setup, I need two instances) to your test specs in before/beforeEach hooks to start a separate instance of your AUT. A code line related to the new instance creation is placed in the `config.ts` file. After that, you can start applications using these instances.
let firstUser: Sp, secondUser: Sp;

beforeEach(async function () {

    firstUser = new Sp(config.userOne);
    secondUser = new Sp(config.userTwo);
……
        await firstUser.startApp();
        await secondUser.startApp();
The approach will simplify the code refactoring in the future. You will place all core methods in files that refer to the page classes and specs.

Creating pageFactory class

As I previously mentioned, I’ve used Page Object pattern and to decrease imports in my files with scenarios I’ve created pageFactory class with all pages. One important thing is that pageFactory has a constructor that is waiting for my app instance to use those locators/methods regardless of another instance of the application.
spectron: Sp;

constructor(spectron: Sp) {
    this.spectron = spectron;
}

loadPages() {
    return {
        loginPage: new LoginPage(this.spectron),
        messagesPage: new MessagesPage(this.spectron),
…..
    }
}
And next snip of code is part of the ‘before’ hook that is placed before starting the app to load pages:
this.pages = [
    firstUser,
    secondUser
];
And following snip to link all page objects to specific instance:
let firstInstance = new PageFactory(firstUser).loadPages();
let secondInstance = new PageFactory(secondUser).loadPages();
The next part is, to be honest, boring because I was adding locators, collecting these locators and methods under them in complex methods, and developing my test scenarios. Before I faced file uploading, I thought I’d slain that particular dragon, and everything else will be straightforward for me like Web automation testing…

Now let’s take a step back, and I will describe some sweets that I found on my way.

I will start from the entry point of our application. I’ve already said that we have Chromium under the Electron wrapper, but we should check the ways how to start the app.
My example is for macOS, but it should be the same on Windows and Linux. Therefore, we can use installed applications under the system and set the following path as an entry point: `/Applications/Application.app/Contents/MacOS/AUT.`
In this case, we will use the provided *dmg file. From the other side, if we have access to source files or to application dist that was built, we can easily use the path to `main.js` file. It can be used as an entry point, like in case of installing the app.

How to run two separate applications

The first requirement to start two separate applications and configure interacting between them without hardcore sleep methods :)
To run two instances, we just need to install two applications and set paths to them with different ChromeDriver ports. The same thing for generated application dist — just set the paths with different ChromeDriver ports. Make sure that it’s possible to run two applications that will look at the same local database.
You can try to run a second instance of an application by using ` — second` or ` — secondary` line, it should look like `./dist/main.js — second` in the path to the application.

How to upload without <input>

So my team provided me the sources, and I’ve started using the approach with generated dist to have actual application version… then I’ve faced an issue #2. `By security assumptions application doesn’t have <input> tag for uploading files within it.`
I would say
and tried `uploadFile` and `chooseFile` WDIO methods. But I failed. So Google helped, and I found an amazing library called spectron-fake-dialog. It has a simple tutorial. I need to use:
fakeDialog.mock([ { method: ‘showOpenDialog’, value: [‘file_for_uploading’] } ])
command and click on the upload button to make us happy.
Unfortunately, this awesome library is good only if you use a single instance of your application. It doesn’t help in my case as I have two applications. The apps start freezing and become unresponsive. “OK Google, what should I do?”.
My Google research doesn’t, however, give any results.
So let’s look underneath the hood. I reviewed the library files and investigated why I was able to mock uploading for a single application but was unable to do it for two instances of the application.
The library contains two files `index.js` and `preload.js`. The `preload.js` generates a mock object.
The `index.js` is used for sending this mock file by clicking on the ‘upload_file’ button. Let’s try to upgrade these two files.
I see that `index.js` file exports `apply` and `mock` functions, but to mock, I need to add the return value for my `mock` function.
To make it happen, I need to copy the `index.js` and `preload.js` to our mock folder and modify the `index.js` file to ensure that we return mock. I’ve updated the `module.exports = { apply, mock };` line to `return { mock }`.
So, now we can use the `mock` method for two instances, and each instance will see this method as its own. This will avoid inconsistencies between calls of the `mock` method.
One more thing that should be updated is a method `apply.` This method contains a call for loading `preload.js,` but I would suggest removing this method.
Here is updated `index.js`:
export default function createMock(app) {
  async function mock(options) {
    return app.electron.ipcRenderer.sendSync('SPECTRON_FAKE_DIALOG/SEND', options);
  }
  return {
    mock
  }
}
We need to remove it because after executing the single test, you won’t face any problems, but if you’ll try to run multiple tests you will see that start parameter contains `’ — require’, ‘../src/test/core/mock-dialog/preload.js’` values as many times as you have executed scenarios.

Conclusion

The conclusion is obvious… There are a lot of applications with different architecture implementations, but each app can be automated. To make it happen, you need to find a driver that can communicate with your type of application. These drivers might not be ideal. But you can always create your own solution for the challenges. In my case, I rewrote the library, but the solution will work for the apps which act in pairs. I believe that only a small number of issues will be relevant in the future, because the desktop apps are coming back in a new form, and the automation industry is evolving very quickly.
As automation engineers, we should be ready to automate such cases. We should move with the times, to find and resolve new issues that occur while developing new frameworks.
This article is a practical guide written by DeviQA software testing automation specialist.

Tags

Topics of interest