Testing Electron Auto-Updater: Verifying Update Flows Before Shipping
Auto-update is one of the most critical features in an Electron app and one of the least tested. When auto-update breaks, users get stuck on old versions — sometimes permanently. When auto-update has a bug, you can push a broken update that prevents future updates from working.
Testing auto-update thoroughly before each release is non-negotiable. This guide covers how.
How electron-updater Works
electron-updater (part of electron-builder) is the standard auto-update library for Electron apps. The flow:
- App calls
autoUpdater.checkForUpdates() - Library requests
latest.yml(orlatest-mac.yml,latest-linux.yml) from your update server - If a new version is available,
update-availableevent fires autoUpdater.downloadUpdate()downloads the installerdownload-progressevents fire with progress dataupdate-downloadedfires when completeautoUpdater.quitAndInstall()restarts the app and installs the update
Each step can fail, and each step is testable.
Unit Testing the Update Service
Extract your update logic into a service class:
// main/services/update-service.js
const { autoUpdater } = require('electron-updater');
const log = require('electron-log');
class UpdateService {
constructor(options = {}) {
this.autoUpdater = options.autoUpdater || autoUpdater;
this.logger = options.logger || log;
this.mainWindow = null;
this._setupListeners();
}
setMainWindow(window) {
this.mainWindow = window;
}
_setupListeners() {
this.autoUpdater.on('checking-for-update', () => {
this.logger.info('Checking for update...');
this._notifyRenderer('update-checking');
});
this.autoUpdater.on('update-available', (info) => {
this.logger.info('Update available:', info.version);
this._notifyRenderer('update-available', info);
});
this.autoUpdater.on('update-not-available', (info) => {
this.logger.info('App is up to date:', info.version);
this._notifyRenderer('update-not-available', info);
});
this.autoUpdater.on('download-progress', (progress) => {
this._notifyRenderer('update-progress', progress);
});
this.autoUpdater.on('update-downloaded', (info) => {
this.logger.info('Update downloaded:', info.version);
this._notifyRenderer('update-downloaded', info);
});
this.autoUpdater.on('error', (error) => {
this.logger.error('Auto-update error:', error.message);
this._notifyRenderer('update-error', { message: error.message });
});
}
_notifyRenderer(channel, data) {
if (this.mainWindow && !this.mainWindow.isDestroyed()) {
this.mainWindow.webContents.send(`updater:${channel}`, data);
}
}
async checkForUpdates() {
return this.autoUpdater.checkForUpdates();
}
async downloadUpdate() {
return this.autoUpdater.downloadUpdate();
}
quitAndInstall() {
this.autoUpdater.quitAndInstall(false, true);
}
}
module.exports = UpdateService;// tests/main/services/update-service.test.js
const EventEmitter = require('events');
class MockAutoUpdater extends EventEmitter {
constructor() {
super();
this.checkForUpdates = jest.fn();
this.downloadUpdate = jest.fn();
this.quitAndInstall = jest.fn();
}
}
describe('UpdateService', () => {
let service;
let mockUpdater;
let mockWindow;
beforeEach(() => {
mockUpdater = new MockAutoUpdater();
mockWindow = {
isDestroyed: jest.fn().mockReturnValue(false),
webContents: {
send: jest.fn(),
},
};
const UpdateService = require('../../../main/services/update-service');
service = new UpdateService({ autoUpdater: mockUpdater });
service.setMainWindow(mockWindow);
});
describe('update-available event', () => {
it('notifies renderer when update is available', () => {
const updateInfo = { version: '2.0.0', releaseDate: '2025-01-15' };
mockUpdater.emit('update-available', updateInfo);
expect(mockWindow.webContents.send).toHaveBeenCalledWith(
'updater:update-available',
updateInfo
);
});
});
describe('download-progress event', () => {
it('forwards progress data to renderer', () => {
const progress = {
bytesPerSecond: 500000,
percent: 45.5,
transferred: 4550000,
total: 10000000,
};
mockUpdater.emit('download-progress', progress);
expect(mockWindow.webContents.send).toHaveBeenCalledWith(
'updater:update-progress',
progress
);
});
});
describe('error handling', () => {
it('notifies renderer of update error', () => {
const error = new Error('net::ERR_INTERNET_DISCONNECTED');
mockUpdater.emit('error', error);
expect(mockWindow.webContents.send).toHaveBeenCalledWith(
'updater:update-error',
{ message: 'net::ERR_INTERNET_DISCONNECTED' }
);
});
it('does not crash when main window is destroyed', () => {
mockWindow.isDestroyed.mockReturnValue(true);
// Should not throw when window is destroyed
expect(() => {
mockUpdater.emit('update-available', { version: '2.0.0' });
}).not.toThrow();
expect(mockWindow.webContents.send).not.toHaveBeenCalled();
});
});
describe('quitAndInstall', () => {
it('calls quitAndInstall with correct parameters', () => {
service.quitAndInstall();
// false = don't force quit immediately, true = install on restart
expect(mockUpdater.quitAndInstall).toHaveBeenCalledWith(false, true);
});
});
});Testing Against a Mock Update Server
For integration testing, run a local update server that serves controlled update data:
// tests/fixtures/update-server.js
const express = require('express');
const path = require('path');
function createMockUpdateServer(options = {}) {
const app = express();
const { latestVersion = '2.0.0', hasUpdate = true } = options;
// Serve the latest.yml file that electron-updater requests
app.get('/latest.yml', (req, res) => {
if (!hasUpdate) {
// Return current version — no update available
res.status(404).send('Not found');
return;
}
res.type('text/yaml').send(`
version: ${latestVersion}
files:
- url: app-${latestVersion}-setup.exe
sha512: abc123def456
size: 75000000
path: app-${latestVersion}-setup.exe
sha512: abc123def456
releaseDate: '2025-01-15T00:00:00.000Z'
`);
});
// Serve fake installer (just bytes for download testing)
app.get(`/app-${latestVersion}-setup.exe`, (req, res) => {
// Stream fake bytes for download progress testing
res.setHeader('Content-Length', '1000000');
let sent = 0;
const interval = setInterval(() => {
const chunk = Buffer.alloc(10000);
res.write(chunk);
sent += 10000;
if (sent >= 1000000) {
clearInterval(interval);
res.end();
}
}, 10);
});
return app;
}
module.exports = { createMockUpdateServer };// tests/integration/auto-update.test.js
const http = require('http');
const { createMockUpdateServer } = require('../fixtures/update-server');
const UpdateService = require('../../main/services/update-service');
describe('auto-update integration', () => {
let server;
let serverPort;
beforeEach(async () => {
// Start mock update server
const app = createMockUpdateServer({ latestVersion: '2.0.0', hasUpdate: true });
server = http.createServer(app);
await new Promise(resolve => server.listen(0, resolve));
serverPort = server.address().port;
// Configure electron-updater to use our mock server
process.env.UPDATE_SERVER_URL = `http://localhost:${serverPort}`;
});
afterEach(async () => {
await new Promise(resolve => server.close(resolve));
});
it('detects available update from server', async () => {
const { autoUpdater } = require('electron-updater');
autoUpdater.setFeedURL({ url: `http://localhost:${serverPort}` });
const updateInfo = await new Promise((resolve, reject) => {
autoUpdater.once('update-available', resolve);
autoUpdater.once('update-not-available', () => reject(new Error('No update found')));
autoUpdater.once('error', reject);
autoUpdater.checkForUpdates();
});
expect(updateInfo.version).toBe('2.0.0');
});
it('reports no update when server returns 404', async () => {
const app = createMockUpdateServer({ hasUpdate: false });
// Use a different port for this test
const noUpdateInfo = await new Promise((resolve, reject) => {
autoUpdater.once('update-not-available', resolve);
autoUpdater.once('update-available', () => reject(new Error('Update found unexpectedly')));
autoUpdater.once('error', reject);
autoUpdater.checkForUpdates();
});
expect(noUpdateInfo).toBeDefined();
});
it('fires download-progress events during download', async () => {
const progressEvents = [];
autoUpdater.once('update-available', () => autoUpdater.downloadUpdate());
autoUpdater.on('download-progress', p => progressEvents.push(p.percent));
await new Promise((resolve, reject) => {
autoUpdater.once('update-downloaded', resolve);
autoUpdater.once('error', reject);
autoUpdater.checkForUpdates();
});
expect(progressEvents.length).toBeGreaterThan(0);
expect(progressEvents[progressEvents.length - 1]).toBe(100);
});
});Testing Update Server Configuration
A common production failure: the update server URL is wrong for a platform or architecture:
// tests/update-server-config.test.js
const { getUpdateServerUrl } = require('../../main/config/update-config');
describe('update server URL configuration', () => {
const testCases = [
{ platform: 'win32', arch: 'x64', expected: /latest\.yml$/ },
{ platform: 'darwin', arch: 'x64', expected: /latest-mac\.yml$/ },
{ platform: 'darwin', arch: 'arm64', expected: /latest-mac\.yml$/ },
{ platform: 'linux', arch: 'x64', expected: /latest-linux\.yml$/ },
];
for (const { platform, arch, expected } of testCases) {
it(`${platform}/${arch} uses correct update manifest`, () => {
// Mock the platform
Object.defineProperty(process, 'platform', { value: platform });
Object.defineProperty(process, 'arch', { value: arch });
const url = getUpdateServerUrl();
expect(url).toMatch(expected);
});
}
});Testing the UI Update Flow
Test that the renderer correctly shows update status:
// tests/components/UpdateBanner.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import UpdateBanner from '../../src/components/UpdateBanner';
beforeEach(() => {
window.electronAPI = {
installUpdate: jest.fn(),
dismissUpdate: jest.fn(),
onUpdateStatus: jest.fn(),
};
});
describe('UpdateBanner', () => {
it('shows download progress', () => {
window.electronAPI.onUpdateStatus.mockImplementation((callback) => {
callback({ type: 'update-progress', percent: 65 });
});
render(<UpdateBanner />);
expect(screen.getByRole('progressbar')).toHaveAttribute('aria-valuenow', '65');
expect(screen.getByText('65%')).toBeInTheDocument();
});
it('shows install button when update is downloaded', () => {
window.electronAPI.onUpdateStatus.mockImplementation((callback) => {
callback({ type: 'update-downloaded', version: '2.0.0' });
});
render(<UpdateBanner />);
expect(screen.getByRole('button', { name: /install/i })).toBeVisible();
expect(screen.getByText('Version 2.0.0 is ready to install')).toBeInTheDocument();
});
it('calls installUpdate when install button clicked', () => {
window.electronAPI.onUpdateStatus.mockImplementation((callback) => {
callback({ type: 'update-downloaded', version: '2.0.0' });
});
render(<UpdateBanner />);
fireEvent.click(screen.getByRole('button', { name: /install/i }));
expect(window.electronAPI.installUpdate).toHaveBeenCalled();
});
it('shows error state when update fails', () => {
window.electronAPI.onUpdateStatus.mockImplementation((callback) => {
callback({ type: 'update-error', message: 'Network error' });
});
render(<UpdateBanner />);
expect(screen.getByText(/failed to download/i)).toBeVisible();
expect(screen.getByRole('button', { name: /retry/i })).toBeVisible();
});
});Summary
Auto-update testing has four layers:
- Unit tests: Test
UpdateServiceevent handling with a mockEventEmitterforautoUpdater - Mock update server: Integration test the full check-and-download flow against a local HTTP server
- Configuration tests: Verify the update server URL is correct for each platform
- UI tests: Verify the renderer shows correct status for each update state
The most critical tests are: update check correctly identifies available updates, download progress fires correctly, and the window-destroyed guard prevents crashes during shutdown. These catch the majority of auto-update bugs before release.