<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Sandumini Karunarathne on Medium]]></title>
        <description><![CDATA[Stories by Sandumini Karunarathne on Medium]]></description>
        <link>https://medium.com/@imanthasandaumini1?source=rss-3fd07f7d72c1------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*1iLlZ2crx3cmwbBiGVGn8A.png</url>
            <title>Stories by Sandumini Karunarathne on Medium</title>
            <link>https://medium.com/@imanthasandaumini1?source=rss-3fd07f7d72c1------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 22 May 2026 13:28:24 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@imanthasandaumini1/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Playwright: Modern Web Testing Framework]]></title>
            <link>https://medium.com/@imanthasandaumini1/playwright-modern-web-testing-framework-0b19613dc501?source=rss-3fd07f7d72c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/0b19613dc501</guid>
            <dc:creator><![CDATA[Sandumini Karunarathne]]></dc:creator>
            <pubDate>Sat, 25 Oct 2025 16:59:04 GMT</pubDate>
            <atom:updated>2025-10-25T17:09:55.425Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/776/1*ZyZlMy5qZJAp4zB8rDubRw.png" /></figure><p>Playwright, developed by Microsoft, represents the next generation of browser automation. Built from the ground up with modern web applications in mind, it addresses many limitations of traditional tools while providing powerful features for reliable end-to-end testing.</p><h3>Why Playwright?</h3><h3>Key Advantages Over Selenium</h3><ol><li><strong>Auto-waiting</strong>: Built-in smart waits eliminate flakiness</li><li><strong>Multi-browser support</strong>: Chromium, Firefox, WebKit with single API</li><li><strong>Mobile emulation</strong>: Built-in device emulation</li><li><strong>Network interception</strong>: Mock APIs and capture network traffic</li><li><strong>Parallel execution</strong>: True parallel testing out of the box</li><li><strong>Fast execution</strong>: Significantly quicker than Selenium</li><li><strong>Multiple contexts</strong>: Isolated browser contexts for parallel scenarios</li><li><strong>Better debugging</strong>: Trace viewer, video recording, screenshots</li><li><strong>Native shadow DOM support</strong>: No special handling needed</li></ol><h3>Installation and Setup</h3><h3>Node.js/TypeScript (Most Common)</h3><pre>npm init playwright@latest</pre><p>This creates:</p><pre>playwright-tests/<br>├── tests/<br>│   └── example.spec.ts<br>├── playwright.config.ts<br>├── package.json<br>└── package-lock.json</pre><h3>Java Setup</h3><pre>&lt;dependency&gt;<br>    &lt;groupId&gt;com.microsoft.playwright&lt;/groupId&gt;<br>    &lt;artifactId&gt;playwright&lt;/artifactId&gt;<br>    &lt;version&gt;1.40.0&lt;/version&gt;<br>&lt;/dependency&gt;</pre><pre>import com.microsoft.playwright.*;</pre><pre>public class Example {<br>    public static void main(String[] args) {<br>        try (Playwright playwright = Playwright.create()) {<br>            Browser browser = playwright.chromium().launch();<br>            Page page = browser.newPage();<br>            page.navigate(&quot;https://example.com&quot;);<br>            System.out.println(page.title());<br>        }<br>    }<br>}</pre><h3>Python Setup</h3><pre>pip install playwright<br>playwright install</pre><pre>from playwright.sync_api import sync_playwright</pre><pre>with sync_playwright() as p:<br>    browser = p.chromium.launch()<br>    page = browser.new_page()<br>    page.goto(&quot;https://example.com&quot;)<br>    print(page.title())<br>    browser.close()</pre><h3>Core Concepts</h3><h3>Browser, Context, and Page</h3><p>Playwright’s three-layer architecture:</p><pre>import { test, expect, chromium } from &#39;@playwright/test&#39;;</pre><pre>// Browser - One per browser type<br>const browser = await chromium.launch({<br>    headless: false,<br>    slowMo: 100  // Slow down by 100ms for visibility<br>});</pre><pre>// Browser Context - Isolated session (like incognito)<br>const context = await browser.newContext({<br>    viewport: { width: 1920, height: 1080 },<br>    locale: &#39;en-US&#39;,<br>    geolocation: { latitude: 40.7128, longitude: -74.0060 },<br>    permissions: [&#39;geolocation&#39;]<br>});</pre><pre>// Page - Individual tab<br>const page = await context.newPage();<br>await page.goto(&#39;https://example.com&#39;);</pre><pre>// Multiple contexts = isolated sessions<br>const context2 = await browser.newContext(); // Different session, different cookies<br>const page2 = await context2.newPage();</pre><p><strong>Why contexts matter</strong>: Test different user sessions in parallel without browser restarts.</p><h3>Locator Strategies</h3><h3>Built-in Locators (Recommended)</h3><p>Playwright’s locators are auto-waiting and auto-retrying:</p><pre>// By role (most resilient)<br>await page.getByRole(&#39;button&#39;, { name: &#39;Submit&#39; }).click();<br>await page.getByRole(&#39;textbox&#39;, { name: &#39;Email&#39; }).fill(&#39;test@example.com&#39;);<br>await page.getByRole(&#39;heading&#39;, { name: &#39;Login&#39; }).isVisible();</pre><pre>// By label text<br>await page.getByLabel(&#39;Email address&#39;).fill(&#39;user@example.com&#39;);<br>await page.getByLabel(&#39;Password&#39;).fill(&#39;secret&#39;);</pre><pre>// By placeholder<br>await page.getByPlaceholder(&#39;Enter your email&#39;).fill(&#39;test@example.com&#39;);</pre><pre>// By text content<br>await page.getByText(&#39;Welcome back&#39;).isVisible();<br>await page.getByText(/welcome back/i).isVisible(); // Case insensitive</pre><pre>// By test ID (best for dynamic content)<br>await page.getByTestId(&#39;submit-button&#39;).click();</pre><pre>// By alt text (images)<br>await page.getByAltText(&#39;Company logo&#39;).isVisible();</pre><pre>// By title<br>await page.getByTitle(&#39;Close dialog&#39;).click();</pre><h3>CSS and XPath (When Needed)</h3><pre>// CSS selector<br>await page.locator(&#39;css=button.submit&#39;).click();<br>await page.locator(&#39;.login-form input[name=&quot;email&quot;]&#39;).fill(&#39;test@example.com&#39;);</pre><pre>// XPath<br>await page.locator(&#39;xpath=//button[text()=&quot;Submit&quot;]&#39;).click();</pre><pre>// Combining locators<br>await page.locator(&#39;.product-list&#39;)<br>    .locator(&#39;.product-item&#39;)<br>    .filter({ hasText: &#39;Laptop&#39; })<br>    .getByRole(&#39;button&#39;, { name: &#39;Add to cart&#39; })<br>    .click();</pre><h3>Locator Filtering and Chaining</h3><pre>// Filter by text<br>await page.getByRole(&#39;listitem&#39;)<br>    .filter({ hasText: &#39;Product 1&#39; })<br>    .click();</pre><pre>// Filter by NOT having text<br>await page.getByRole(&#39;listitem&#39;)<br>    .filter({ hasNotText: &#39;Sold out&#39; })<br>    .first()<br>    .click();</pre><pre>// Chain locators<br>await page.locator(&#39;.product-list&#39;)<br>    .getByRole(&#39;article&#39;)<br>    .filter({ has: page.getByText(&#39;In stock&#39;) })<br>    .getByRole(&#39;button&#39;, { name: &#39;Buy&#39; })<br>    .click();</pre><pre>// Get nth element<br>await page.getByRole(&#39;listitem&#39;).nth(2).click();</pre><pre>// Get first/last<br>await page.getByRole(&#39;listitem&#39;).first().click();<br>await page.getByRole(&#39;listitem&#39;).last().click();</pre><pre>// Count elements<br>const count = await page.getByRole(&#39;listitem&#39;).count();</pre><h3>Auto-Waiting: The Killer Feature</h3><p>Playwright automatically waits for elements before performing actions:</p><pre>// No explicit waits needed!<br>await page.getByRole(&#39;button&#39;).click();  // Waits for: attached, visible, stable, enabled<br>await page.getByLabel(&#39;Email&#39;).fill(&#39;test@example.com&#39;);  // Waits for: attached, visible, enabled<br>await page.getByRole(&#39;link&#39;).click();  // Waits for: attached, visible, stable, enabled</pre><pre>// Assertions also auto-wait<br>await expect(page.getByText(&#39;Success&#39;)).toBeVisible();  // Waits up to 5s by default<br>await expect(page.getByRole(&#39;button&#39;)).toBeEnabled();<br>await expect(page.locator(&#39;.loading&#39;)).toBeHidden();</pre><h3>Custom Timeouts</h3><pre>// Change default timeout<br>test.setTimeout(60000); // 60 seconds</pre><pre>// Per action timeout<br>await page.getByRole(&#39;button&#39;).click({ timeout: 10000 });</pre><pre>// Assertion timeout<br>await expect(page.getByText(&#39;Loading...&#39;)).toBeHidden({ timeout: 30000 });</pre><pre>// Navigation timeout<br>await page.goto(&#39;https://example.com&#39;, { timeout: 30000 });</pre><pre>// Wait for specific condition<br>await page.waitForSelector(&#39;.data-loaded&#39;);<br>await page.waitForLoadState(&#39;networkidle&#39;);<br>await page.waitForURL(&#39;**/dashboard&#39;);<br>await page.waitForFunction(() =&gt; window.dataLoaded === true);</pre><h3>Assertions</h3><h3>Built-in Expect Assertions</h3><pre>import { expect } from &#39;@playwright/test&#39;;</pre><pre>// Visibility<br>await expect(page.getByText(&#39;Welcome&#39;)).toBeVisible();<br>await expect(page.getByText(&#39;Loading&#39;)).toBeHidden();</pre><pre>// Enabled/Disabled<br>await expect(page.getByRole(&#39;button&#39;)).toBeEnabled();<br>await expect(page.getByRole(&#39;button&#39;)).toBeDisabled();</pre><pre>// Text content<br>await expect(page.getByRole(&#39;heading&#39;)).toHaveText(&#39;Dashboard&#39;);<br>await expect(page.getByRole(&#39;heading&#39;)).toContainText(&#39;Dash&#39;);</pre><pre>// Value<br>await expect(page.getByLabel(&#39;Email&#39;)).toHaveValue(&#39;test@example.com&#39;);</pre><pre>// Attribute<br>await expect(page.getByRole(&#39;link&#39;)).toHaveAttribute(&#39;href&#39;, &#39;/dashboard&#39;);</pre><pre>// CSS class<br>await expect(page.getByRole(&#39;button&#39;)).toHaveClass(/btn-primary/);</pre><pre>// Count<br>await expect(page.getByRole(&#39;listitem&#39;)).toHaveCount(5);</pre><pre>// URL<br>await expect(page).toHaveURL(&#39;https://example.com/dashboard&#39;);<br>await expect(page).toHaveURL(/dashboard/);</pre><pre>// Title<br>await expect(page).toHaveTitle(&#39;Dashboard | MyApp&#39;);</pre><pre>// Screenshot comparison<br>await expect(page).toHaveScreenshot(&#39;homepage.png&#39;);</pre><h3>Soft Assertions</h3><p>Continue test execution even if the assertion fails:</p><pre>await expect.soft(page.getByText(&#39;Header&#39;)).toBeVisible();<br>await expect.soft(page.getByText(&#39;Footer&#39;)).toBeVisible();<br>// Both assertions will be checked even if first fails</pre><h3>Page Object Model in Playwright</h3><pre>// pages/LoginPage.ts<br>import { Page, Locator } from &#39;@playwright/test&#39;;</pre><pre>export class LoginPage {<br>    readonly page: Page;<br>    readonly emailInput: Locator;<br>    readonly passwordInput: Locator;<br>    readonly submitButton: Locator;<br>    readonly errorMessage: Locator;</pre><pre>    constructor(page: Page) {<br>        this.page = page;<br>        this.emailInput = page.getByLabel(&#39;Email address&#39;);<br>        this.passwordInput = page.getByLabel(&#39;Password&#39;);<br>        this.submitButton = page.getByRole(&#39;button&#39;, { name: &#39;Sign in&#39; });<br>        this.errorMessage = page.getByRole(&#39;alert&#39;);<br>    }</pre><pre>    async goto() {<br>        await this.page.goto(&#39;/login&#39;);<br>    }</pre><pre>    async login(email: string, password: string) {<br>        await this.emailInput.fill(email);<br>        await this.passwordInput.fill(password);<br>        await this.submitButton.click();<br>    }</pre><pre>    async getErrorMessage() {<br>        return await this.errorMessage.textContent();<br>    }<br>}</pre><pre>// tests/login.spec.ts<br>import { test, expect } from &#39;@playwright/test&#39;;<br>import { LoginPage } from &#39;../pages/LoginPage&#39;;</pre><pre>test(&#39;successful login&#39;, async ({ page }) =&gt; {<br>    const loginPage = new LoginPage(page);<br>    await loginPage.goto();<br>    await loginPage.login(&#39;user@example.com&#39;, &#39;password123&#39;);<br>    await expect(page).toHaveURL(&#39;/dashboard&#39;);<br>});</pre><pre>test(&#39;login with invalid credentials&#39;, async ({ page }) =&gt; {<br>    const loginPage = new LoginPage(page);<br>    await loginPage.goto();<br>    await loginPage.login(&#39;invalid@example.com&#39;, &#39;wrong&#39;);<br>    const error = await loginPage.getErrorMessage();<br>    expect(error).toContain(&#39;Invalid credentials&#39;);<br>});</pre><h3>Fixtures: Reusable Test Setup</h3><p>Fixtures provide reusable setup and teardown:</p><pre>// fixtures/basePage.ts<br>import { test as base } from &#39;@playwright/test&#39;;<br>import { LoginPage } from &#39;../pages/LoginPage&#39;;<br>import { DashboardPage } from &#39;../pages/DashboardPage&#39;;</pre><pre>type MyFixtures = {<br>    loginPage: LoginPage;<br>    dashboardPage: DashboardPage;<br>    authenticatedPage: Page;<br>};</pre><pre>export const test = base.extend&lt;MyFixtures&gt;({<br>    loginPage: async ({ page }, use) =&gt; {<br>        await use(new LoginPage(page));<br>    },<br>    <br>    dashboardPage: async ({ page }, use) =&gt; {<br>        await use(new DashboardPage(page));<br>    },<br>    <br>    // Auto-login fixture<br>    authenticatedPage: async ({ page }, use) =&gt; {<br>        const loginPage = new LoginPage(page);<br>        await loginPage.goto();<br>        await loginPage.login(&#39;test@example.com&#39;, &#39;password123&#39;);<br>        await page.waitForURL(&#39;/dashboard&#39;);<br>        await use(page);<br>    }<br>});</pre><pre>export { expect } from &#39;@playwright/test&#39;;</pre><pre>// tests/dashboard.spec.ts<br>import { test, expect } from &#39;../fixtures/basePage&#39;;</pre><pre>test(&#39;view dashboard as authenticated user&#39;, async ({ authenticatedPage }) =&gt; {<br>    // Already logged in!<br>    await expect(authenticatedPage.getByRole(&#39;heading&#39;)).toHaveText(&#39;Dashboard&#39;);<br>});</pre><h3>Network Interception and Mocking</h3><h3>Mock API Responses</h3><pre>test(&#39;mock API response&#39;, async ({ page }) =&gt; {<br>    // Intercept and mock API call<br>    await page.route(&#39;**/api/users&#39;, route =&gt; {<br>        route.fulfill({<br>            status: 200,<br>            contentType: &#39;application/json&#39;,<br>            body: JSON.stringify([<br>                { id: 1, name: &#39;John Doe&#39; },<br>                { id: 2, name: &#39;Jane Smith&#39; }<br>            ])<br>        });<br>    });<br>    <br>    await page.goto(&#39;/users&#39;);<br>    await expect(page.getByText(&#39;John Doe&#39;)).toBeVisible();<br>});</pre><pre>// Mock with different status<br>test(&#39;handle API error&#39;, async ({ page }) =&gt; {<br>    await page.route(&#39;**/api/users&#39;, route =&gt; {<br>        route.fulfill({<br>            status: 500,<br>            body: &#39;Internal Server Error&#39;<br>        });<br>    });<br>    <br>    await page.goto(&#39;/users&#39;);<br>    await expect(page.getByText(&#39;Failed to load users&#39;)).toBeVisible();<br>});</pre><h3>Abort Requests</h3><pre>test(&#39;block images and stylesheets&#39;, async ({ page }) =&gt; {<br>    await page.route(&#39;**/*.{png,jpg,jpeg,css}&#39;, route =&gt; route.abort());<br>    await page.goto(&#39;https://example.com&#39;);<br>});</pre><h3>Modify Requests</h3><pre>test(&#39;modify request headers&#39;, async ({ page }) =&gt; {<br>    await page.route(&#39;**/api/**&#39;, route =&gt; {<br>        const headers = {<br>            ...route.request().headers(),<br>            &#39;Authorization&#39;: &#39;Bearer fake-token&#39;<br>        };<br>        route.continue({ headers });<br>    });<br>    <br>    await page.goto(&#39;/dashboard&#39;);<br>});</pre><h3>Network Monitoring</h3><pre>test(&#39;capture network requests&#39;, async ({ page }) =&gt; {<br>    const requests: string[] = [];<br>    <br>    page.on(&#39;request&#39;, request =&gt; {<br>        requests.push(request.url());<br>    });<br>    <br>    page.on(&#39;response&#39;, response =&gt; {<br>        console.log(`${response.status()} ${response.url()}`);<br>    });<br>    <br>    await page.goto(&#39;https://example.com&#39;);<br>    console.log(&#39;All requests:&#39;, requests);<br>});</pre><h3>Browser Contexts for Parallel Testing</h3><h3>Multiple Users Simultaneously</h3><pre>test(&#39;multiple users in parallel&#39;, async ({ browser }) =&gt; {<br>    // User 1 - Admin<br>    const adminContext = await browser.newContext();<br>    const adminPage = await adminContext.newPage();<br>    await adminPage.goto(&#39;/login&#39;);<br>    await adminPage.getByLabel(&#39;Email&#39;).fill(&#39;admin@example.com&#39;);<br>    await adminPage.getByLabel(&#39;Password&#39;).fill(&#39;admin123&#39;);<br>    await adminPage.getByRole(&#39;button&#39;, { name: &#39;Login&#39; }).click();<br>    <br>    // User 2 - Regular user<br>    const userContext = await browser.newContext();<br>    const userPage = await userContext.newPage();<br>    await userPage.goto(&#39;/login&#39;);<br>    await userPage.getByLabel(&#39;Email&#39;).fill(&#39;user@example.com&#39;);<br>    await userPage.getByLabel(&#39;Password&#39;).fill(&#39;user123&#39;);<br>    await userPage.getByRole(&#39;button&#39;, { name: &#39;Login&#39; }).click();<br>    <br>    // Both users interact simultaneously<br>    await adminPage.getByRole(&#39;link&#39;, { name: &#39;Admin Panel&#39; }).click();<br>    await userPage.getByRole(&#39;link&#39;, { name: &#39;Profile&#39; }).click();<br>    <br>    await expect(adminPage.getByRole(&#39;heading&#39;)).toHaveText(&#39;Admin Panel&#39;);<br>    await expect(userPage.getByRole(&#39;heading&#39;)).toHaveText(&#39;My Profile&#39;);<br>    <br>    await adminContext.close();<br>    await userContext.close();<br>});</pre><h3>State Reuse (Storage State)</h3><p>Save authentication state and reuse:</p><pre>// global-setup.ts<br>import { chromium, FullConfig } from &#39;@playwright/test&#39;;</pre><pre>async function globalSetup(config: FullConfig) {<br>    const browser = await chromium.launch();<br>    const page = await browser.newPage();<br>    <br>    await page.goto(&#39;https://example.com/login&#39;);<br>    await page.getByLabel(&#39;Email&#39;).fill(&#39;test@example.com&#39;);<br>    await page.getByLabel(&#39;Password&#39;).fill(&#39;password123&#39;);<br>    await page.getByRole(&#39;button&#39;, { name: &#39;Login&#39; }).click();<br>    await page.waitForURL(&#39;/dashboard&#39;);<br>    <br>    // Save authentication state<br>    await page.context().storageState({ path: &#39;auth.json&#39; });<br>    await browser.close();<br>}</pre><pre>export default globalSetup;</pre><pre>// playwright.config.ts<br>export default defineConfig({<br>    globalSetup: require.resolve(&#39;./global-setup&#39;),<br>    use: {<br>        storageState: &#39;auth.json&#39;<br>    }<br>});</pre><pre>// All tests now start authenticated!<br>test(&#39;access dashboard&#39;, async ({ page }) =&gt; {<br>    await page.goto(&#39;/dashboard&#39;);<br>    // Already logged in via storage state<br>    await expect(page.getByRole(&#39;heading&#39;)).toHaveText(&#39;Dashboard&#39;);<br>});</pre><h3>Mobile Emulation</h3><pre>import { devices } from &#39;@playwright/test&#39;;</pre><pre>test(&#39;test on iPhone 13&#39;, async ({ browser }) =&gt; {<br>    const context = await browser.newContext({<br>        ...devices[&#39;iPhone 13&#39;]<br>    });<br>    const page = await context.newPage();<br>    await page.goto(&#39;https://example.com&#39;);<br>    <br>    // Mobile-specific interactions<br>    await page.locator(&#39;.menu-icon&#39;).tap();<br>});</pre><pre>test(&#39;test on iPad&#39;, async ({ browser }) =&gt; {<br>    const context = await browser.newContext({<br>        ...devices[&#39;iPad Pro&#39;]<br>    });<br>    const page = await context.newPage();<br>    await page.goto(&#39;https://example.com&#39;);<br>});</pre><pre>// Custom device<br>test(&#39;custom mobile viewport&#39;, async ({ browser }) =&gt; {<br>    const context = await browser.newContext({<br>        viewport: { width: 375, height: 667 },<br>        userAgent: &#39;Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)&#39;,<br>        isMobile: true,<br>        hasTouch: true<br>    });<br>    const page = await context.newPage();<br>    await page.goto(&#39;https://example.com&#39;);<br>});</pre><h3>Debugging Tools</h3><h3>UI Mode</h3><pre>npx playwright test --ui</pre><p>Interactive mode with time-travel debugging, watch mode, and step-through execution.</p><h3>Trace Viewer</h3><pre>// playwright.config.ts<br>export default defineConfig({<br>    use: {<br>        trace: &#39;on-first-retry&#39;, // or &#39;on&#39;, &#39;off&#39;, &#39;retain-on-failure&#39;<br>        screenshot: &#39;only-on-failure&#39;,<br>        video: &#39;retain-on-failure&#39;<br>    }<br>});</pre><p>View traces:</p><pre>npx playwright show-trace trace.zip</pre><h3>Debug Mode</h3><pre>npx playwright test --debug</pre><p>Step through tests with Playwright Inspector.</p><h3>Codegen: Generate Tests</h3><pre>npx playwright codegen https://example.com</pre><p>Records your actions and generates test code automatically.</p><h3>Console Logs</h3><pre>test(&#39;debug test&#39;, async ({ page }) =&gt; {<br>    page.on(&#39;console&#39;, msg =&gt; console.log(&#39;Browser console:&#39;, msg.text()));<br>    <br>    // Add breakpoint<br>    await page.pause();<br>    <br>    await page.goto(&#39;https://example.com&#39;);<br>});</pre><h3>Advanced Features</h3><h3>File Upload</h3><pre>test(&#39;upload file&#39;, async ({ page }) =&gt; {<br>    await page.goto(&#39;/upload&#39;);<br>    <br>    // Single file<br>    await page.getByLabel(&#39;Upload file&#39;).setInputFiles(&#39;path/to/file.pdf&#39;);<br>    <br>    // Multiple files<br>    await page.getByLabel(&#39;Upload files&#39;).setInputFiles([<br>        &#39;file1.pdf&#39;,<br>        &#39;file2.pdf&#39;<br>    ]);<br>    <br>    // Upload from buffer<br>    await page.getByLabel(&#39;Upload&#39;).setInputFiles({<br>        name: &#39;test.txt&#39;,<br>        mimeType: &#39;text/plain&#39;,<br>        buffer: Buffer.from(&#39;File content&#39;)<br>    });<br>    <br>    // Clear file input<br>    await page.getByLabel(&#39;Upload file&#39;).setInputFiles([]);<br>});</pre><h3>File Download</h3><pre>test(&#39;download file&#39;, async ({ page }) =&gt; {<br>    await page.goto(&#39;/downloads&#39;);<br>    <br>    const downloadPromise = page.waitForEvent(&#39;download&#39;);<br>    await page.getByRole(&#39;button&#39;, { name: &#39;Download report&#39; }).click();<br>    const download = await downloadPromise;<br>    <br>    // Save to specific path<br>    await download.saveAs(&#39;./downloads/&#39; + download.suggestedFilename());<br>    <br>    // Get download stream<br>    const stream = await download.createReadStream();<br>});</pre><h3>Drag and Drop</h3><pre>test(&#39;drag and drop&#39;, async ({ page }) =&gt; {<br>    await page.goto(&#39;/kanban&#39;);<br>    <br>    const source = page.locator(&#39;.task-card&#39;).first();<br>    const target = page.locator(&#39;.column-done&#39;);<br>    <br>    await source.dragTo(target);<br>});</pre><h3>Geolocation</h3><pre>test(&#39;test with geolocation&#39;, async ({ browser }) =&gt; {<br>    const context = await browser.newContext({<br>        geolocation: { latitude: 40.7128, longitude: -74.0060 },<br>        permissions: [&#39;geolocation&#39;]<br>    });<br>    const page = await context.newPage();<br>    await page.goto(&#39;https://example.com/map&#39;);<br>});</pre><h3>Clipboard</h3><pre>test(&#39;copy to clipboard&#39;, async ({ page, context }) =&gt; {<br>    await context.grantPermissions([&#39;clipboard-read&#39;, &#39;clipboard-write&#39;]);<br>    <br>    await page.goto(&#39;/editor&#39;);<br>    await page.getByRole(&#39;button&#39;, { name: &#39;Copy&#39; }).click();<br>    <br>    const clipboardText = await page.evaluate(() =&gt; <br>        navigator.clipboard.readText()<br>    );<br>    expect(clipboardText).toBe(&#39;Expected text&#39;);<br>});</pre><h3>Authentication with API</h3><pre>test(&#39;login via API&#39;, async ({ page, request }) =&gt; {<br>    // Login through API<br>    const response = await request.post(&#39;https://api.example.com/login&#39;, {<br>        data: {<br>            email: &#39;test@example.com&#39;,<br>            password: &#39;password123&#39;<br>        }<br>    });<br>    <br>    const { token } = await response.json();<br>    <br>    // Set token in context<br>    await page.context().addCookies([{<br>        name: &#39;auth_token&#39;,<br>        value: token,<br>        domain: &#39;example.com&#39;,<br>        path: &#39;/&#39;<br>    }]);<br>    <br>    await page.goto(&#39;/dashboard&#39;);<br>    // Already authenticated<br>});</pre><h3>Configuration Best Practices</h3><pre>// playwright.config.ts<br>import { defineConfig, devices } from &#39;@playwright/test&#39;;</pre><pre>export default defineConfig({<br>    testDir: &#39;./tests&#39;,<br>    timeout: 30000,<br>    expect: {<br>        timeout: 5000<br>    },<br>    fullyParallel: true,<br>    forbidOnly: !!process.env.CI,<br>    retries: process.env.CI ? 2 : 0,<br>    workers: process.env.CI ? 1 : undefined,<br>    reporter: [<br>        [&#39;html&#39;],<br>        [&#39;json&#39;, { outputFile: &#39;test-results.json&#39; }],<br>        [&#39;junit&#39;, { outputFile: &#39;junit.xml&#39; }]<br>    ],<br>    <br>    use: {<br>        baseURL: process.env.BASE_URL || &#39;http://localhost:3000&#39;,<br>        trace: &#39;on-first-retry&#39;,<br>        screenshot: &#39;only-on-failure&#39;,<br>        video: &#39;retain-on-failure&#39;,<br>        actionTimeout: 10000,<br>        navigationTimeout: 30000<br>    },<br>    <br>    projects: [<br>        {<br>            name: &#39;chromium&#39;,<br>            use: { ...devices[&#39;Desktop Chrome&#39;] }<br>        },<br>        {<br>            name: &#39;firefox&#39;,<br>            use: { ...devices[&#39;Desktop Firefox&#39;] }<br>        },<br>        {<br>            name: &#39;webkit&#39;,<br>            use: { ...devices[&#39;Desktop Safari&#39;] }<br>        },<br>        {<br>            name: &#39;Mobile Chrome&#39;,<br>            use: { ...devices[&#39;Pixel 5&#39;] }<br>        },<br>        {<br>            name: &#39;Mobile Safari&#39;,<br>            use: { ...devices[&#39;iPhone 13&#39;] }<br>        }<br>    ],<br>    <br>    webServer: {<br>        command: &#39;npm run start&#39;,<br>        url: &#39;http://localhost:3000&#39;,<br>        reuseExistingServer: !process.env.CI,<br>        timeout: 120000<br>    }<br>});</pre><h3>Test Organization Patterns</h3><h3>Group Tests with describe</h3><pre>import { test, expect } from &#39;@playwright/test&#39;;</pre><pre>test.describe(&#39;Login functionality&#39;, () =&gt; {<br>    test.beforeEach(async ({ page }) =&gt; {<br>        await page.goto(&#39;/login&#39;);<br>    });<br>    <br>    test(&#39;successful login&#39;, async ({ page }) =&gt; {<br>        await page.getByLabel(&#39;Email&#39;).fill(&#39;test@example.com&#39;);<br>        await page.getByLabel(&#39;Password&#39;).fill(&#39;password123&#39;);<br>        await page.getByRole(&#39;button&#39;, { name: &#39;Login&#39; }).click();<br>        await expect(page).toHaveURL(&#39;/dashboard&#39;);<br>    });<br>    <br>    test(&#39;failed login&#39;, async ({ page }) =&gt; {<br>        await page.getByLabel(&#39;Email&#39;).fill(&#39;invalid@example.com&#39;);<br>        await page.getByLabel(&#39;Password&#39;).fill(&#39;wrong&#39;);<br>        await page.getByRole(&#39;button&#39;, { name: &#39;Login&#39; }).click();<br>        await expect(page.getByRole(&#39;alert&#39;)).toBeVisible();<br>    });<br>});</pre><pre>test.describe(&#39;User profile&#39;, () =&gt; {<br>    test.use({ storageState: &#39;auth.json&#39; });<br>    <br>    test(&#39;update profile&#39;, async ({ page }) =&gt; {<br>        await page.goto(&#39;/profile&#39;);<br>        await page.getByLabel(&#39;Name&#39;).fill(&#39;New Name&#39;);<br>        await page.getByRole(&#39;button&#39;, { name: &#39;Save&#39; }).click();<br>        await expect(page.getByText(&#39;Profile updated&#39;)).toBeVisible();<br>    });<br>});</pre><h3>Tags and Annotations</h3><pre>test(&#39;critical user flow&#39;, { tag: &#39;@smoke&#39; }, async ({ page }) =&gt; {<br>    // Test code<br>});</pre><pre>test(&#39;slow test&#39;, { tag: &#39;@slow&#39; }, async ({ page }) =&gt; {<br>    test.slow(); // Triples timeout<br>    // Test code<br>});</pre><pre>test.skip(&#39;not implemented yet&#39;, async ({ page }) =&gt; {<br>    // Will be skipped<br>});</pre><pre>test(&#39;flaky test&#39;, async ({ page }) =&gt; {<br>    test.fixme(true, &#39;Known issue: JIRA-123&#39;);<br>});</pre><p>Run tagged tests:</p><pre>npx playwright test --grep @smoke<br>npx playwright test --grep-invert @slow</pre><h3>Best Practices</h3><h3>1. Use Built-in Locators</h3><p>Prefer getByRole, getByLabel, getByText over CSS/XPath for resilience.</p><h3>2. Leverage Auto-waiting</h3><p>Don’t add manual waits. Trust Playwright’s auto-waiting mechanism.</p><h3>3. Use Strict Mode</h3><p>Ensure locators match exactly one element:</p><pre>await page.locator(&#39;button&#39;).click(); // Fails if multiple buttons</pre><h3>4. Network Mocking in Tests</h3><p>Mock external APIs to avoid flaky tests due to network issues.</p><h3>5. Reuse Authentication State</h3><p>Use storageState to avoid repeated logins.</p><h3>6. Parallel Execution</h3><p>Enable fullyParallel: true for faster test runs.</p><h3>7. Use Test Fixtures</h3><p>Create reusable fixtures for common setup patterns.</p><h3>8. Visual Regression Testing</h3><pre>await expect(page).toHaveScreenshot(&#39;homepage.png&#39;, {<br>    maxDiffPixels: 100<br>});</pre><h3>Playwright vs Selenium: When to Choose What</h3><p><strong>Choose Playwright when:</strong></p><ul><li>Starting new projects</li><li>Need modern browser features (Shadow DOM, network interception)</li><li>Want faster execution and less flakiness</li><li>Need built-in parallelization</li><li>Working with modern web frameworks (React, Vue, Angular)</li><li>Need comprehensive debugging tools</li></ul><p><strong>Choose Selenium when:</strong></p><ul><li>Maintaining existing Selenium infrastructure</li><li>Need support for older browsers (IE11)</li><li>Team has deep Selenium expertise</li><li>Using languages without Playwright support</li><li>Require specific Selenium Grid features</li></ul><h3>Conclusion</h3><p>Playwright represents a paradigm shift in web testing. Its auto-waiting mechanism eliminates the most common source of test flakiness, while features like network interception, browser contexts, and comprehensive debugging tools enable testing scenarios that were previously complex or impossible.</p><p>The framework’s modern architecture, combined with powerful features like trace viewer and codegen, reduces the time spent debugging and increases confidence in test results. For new automation projects, Playwright should be the default choice, offering a superior developer experience and more reliable tests than traditional alternatives.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0b19613dc501" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Selenium WebDriver: Advanced Technical Guide]]></title>
            <link>https://medium.com/@imanthasandaumini1/selenium-webdriver-advanced-technical-guide-8cb8426eeda1?source=rss-3fd07f7d72c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/8cb8426eeda1</guid>
            <dc:creator><![CDATA[Sandumini Karunarathne]]></dc:creator>
            <pubDate>Sat, 25 Oct 2025 16:55:07 GMT</pubDate>
            <atom:updated>2025-10-25T17:05:13.763Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/963/1*SS2ikssXQE6f5ZJR5BSGUQ.png" /></figure><p>Selenium WebDriver is the industry-standard tool for browser automation, enabling testers to simulate real user interactions across different browsers. Understanding its architecture, best practices, and advanced features is crucial for building robust web automation frameworks.</p><h3>Selenium WebDriver Architecture</h3><p>WebDriver operates on a client-server architecture:</p><ol><li><strong>Test Script</strong> — Your automation code</li><li><strong>Language Binding</strong> — Selenium library (Java, Python, C#, JavaScript)</li><li><strong>JSON Wire Protocol / W3C WebDriver Protocol</strong> — Communication standard</li><li><strong>Browser Driver</strong> — Browser-specific implementation (ChromeDriver, GeckoDriver, etc.)</li><li><strong>Browser</strong> — Actual browser (Chrome, Firefox, Edge, Safari)</li></ol><pre>Test Code → WebDriver API → Browser Driver → Browser</pre><h3>Setting Up WebDriver</h3><h3>WebDriver Manager (Recommended)</h3><p>Automatically manages browser drivers:</p><pre>import io.github.bonigarcia.wdm.WebDriverManager;<br>import org.openqa.selenium.WebDriver;<br>import org.openqa.selenium.chrome.ChromeDriver;</pre><pre>public class DriverFactory {<br>    public static WebDriver createDriver() {<br>        WebDriverManager.chromedriver().setup();<br>        return new ChromeDriver();<br>    }<br>}</pre><p>For other browsers:</p><pre>// Firefox<br>WebDriverManager.firefoxdriver().setup();<br>WebDriver driver = new FirefoxDriver();</pre><pre>// Edge<br>WebDriverManager.edgedriver().setup();<br>WebDriver driver = new EdgeDriver();</pre><pre>// Safari (no driver needed on macOS)<br>WebDriver driver = new SafariDriver();</pre><h3>ChromeOptions: Customizing Browser Behavior</h3><pre>import org.openqa.selenium.chrome.ChromeOptions;</pre><pre>public class ChromeOptionsConfig {<br>    public static ChromeOptions getChromeOptions() {<br>        ChromeOptions options = new ChromeOptions();<br>        <br>        // Headless mode<br>        options.addArguments(&quot;--headless=new&quot;);<br>        <br>        // Window size<br>        options.addArguments(&quot;--window-size=1920,1080&quot;);<br>        <br>        // Disable notifications<br>        options.addArguments(&quot;--disable-notifications&quot;);<br>        <br>        // Disable GPU (useful in CI)<br>        options.addArguments(&quot;--disable-gpu&quot;);<br>        <br>        // No sandbox (for Docker/CI)<br>        options.addArguments(&quot;--no-sandbox&quot;);<br>        options.addArguments(&quot;--disable-dev-shm-usage&quot;);<br>        <br>        // Disable extensions<br>        options.addArguments(&quot;--disable-extensions&quot;);<br>        <br>        // Set download directory<br>        Map&lt;String, Object&gt; prefs = new HashMap&lt;&gt;();<br>        prefs.put(&quot;download.default_directory&quot;, &quot;/path/to/download&quot;);<br>        prefs.put(&quot;download.prompt_for_download&quot;, false);<br>        options.setExperimentalOption(&quot;prefs&quot;, prefs);<br>        <br>        // Performance logging<br>        LoggingPreferences logPrefs = new LoggingPreferences();<br>        logPrefs.enable(LogType.PERFORMANCE, Level.ALL);<br>        options.setCapability(&quot;goog:loggingPrefs&quot;, logPrefs);<br>        <br>        return options;<br>    }<br>}</pre><h3>Locator Strategies</h3><p>Choosing the right locator strategy is critical for maintainability:</p><h3>Locator Priority (Best to Worst)</h3><ol><li><strong>ID</strong> — Fastest and most reliable</li><li><strong>Name</strong> — Good if unique</li><li><strong>CSS Selector</strong> — Fast and flexible</li><li><strong>XPath</strong> — Powerful but slower</li><li><strong>Link Text / Partial Link Text</strong> — For links only</li><li><strong>Tag Name / Class Name</strong> — Too generic</li></ol><h3>CSS Selectors vs XPath</h3><p><strong>CSS Selectors</strong> (Preferred):</p><pre>// By ID<br>driver.findElement(By.cssSelector(&quot;#username&quot;));</pre><pre>// By class<br>driver.findElement(By.cssSelector(&quot;.btn-primary&quot;));</pre><pre>// By attribute<br>driver.findElement(By.cssSelector(&quot;input[type=&#39;email&#39;]&quot;));</pre><pre>// Descendant<br>driver.findElement(By.cssSelector(&quot;div.form-group input&quot;));</pre><pre>// Direct child<br>driver.findElement(By.cssSelector(&quot;div.header &gt; button&quot;));</pre><pre>// Multiple classes<br>driver.findElement(By.cssSelector(&quot;.btn.btn-primary.btn-large&quot;));</pre><pre>// Attribute contains<br>driver.findElement(By.cssSelector(&quot;button[class*=&#39;submit&#39;]&quot;));</pre><pre>// Attribute starts with<br>driver.findElement(By.cssSelector(&quot;input[id^=&#39;user&#39;]&quot;));</pre><pre>// Attribute ends with<br>driver.findElement(By.cssSelector(&quot;input[id$=&#39;name&#39;]&quot;));</pre><pre>// nth-child<br>driver.findElement(By.cssSelector(&quot;ul &gt; li:nth-child(2)&quot;));</pre><pre>// First/last child<br>driver.findElement(By.cssSelector(&quot;ul &gt; li:first-child&quot;));<br>driver.findElement(By.cssSelector(&quot;ul &gt; li:last-child&quot;));</pre><p><strong>XPath</strong> (When CSS isn’t enough):</p><pre>// By text<br>driver.findElement(By.xpath(&quot;//button[text()=&#39;Submit&#39;]&quot;));</pre><pre>// Contains text<br>driver.findElement(By.xpath(&quot;//button[contains(text(),&#39;Submit&#39;)]&quot;));</pre><pre>// Ancestor<br>driver.findElement(By.xpath(&quot;//input[@id=&#39;email&#39;]/ancestor::form&quot;));</pre><pre>// Following sibling<br>driver.findElement(By.xpath(&quot;//label[text()=&#39;Email&#39;]/following-sibling::input&quot;));</pre><pre>// Preceding sibling<br>driver.findElement(By.xpath(&quot;//input[@id=&#39;email&#39;]/preceding-sibling::label&quot;));</pre><pre>// Multiple conditions<br>driver.findElement(By.xpath(&quot;//button[@type=&#39;submit&#39; and @class=&#39;btn-primary&#39;]&quot;));</pre><pre>// Position<br>driver.findElement(By.xpath(&quot;(//div[@class=&#39;product&#39;])[1]&quot;));</pre><pre>// Not condition<br>driver.findElement(By.xpath(&quot;//button[not(contains(@class,&#39;disabled&#39;))]&quot;));</pre><pre>// Parent<br>driver.findElement(By.xpath(&quot;//input[@id=&#39;email&#39;]/parent::div&quot;));</pre><h3>Wait Strategies</h3><h3>Explicit Waits (Recommended)</h3><pre>import org.openqa.selenium.support.ui.WebDriverWait;<br>import org.openqa.selenium.support.ui.ExpectedConditions;<br>import java.time.Duration;</pre><pre>public class WaitHelper {<br>    private WebDriver driver;<br>    private WebDriverWait wait;<br>    <br>    public WaitHelper(WebDriver driver) {<br>        this.driver = driver;<br>        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));<br>    }<br>    <br>    // Wait for element to be visible<br>    public WebElement waitForVisibility(By locator) {<br>        return wait.until(ExpectedConditions.visibilityOfElementLocated(locator));<br>    }<br>    <br>    // Wait for element to be clickable<br>    public WebElement waitForClickable(By locator) {<br>        return wait.until(ExpectedConditions.elementToBeClickable(locator));<br>    }<br>    <br>    // Wait for element to be present (may not be visible)<br>    public WebElement waitForPresence(By locator) {<br>        return wait.until(ExpectedConditions.presenceOfElementLocated(locator));<br>    }<br>    <br>    // Wait for text to be present<br>    public boolean waitForTextToBe(By locator, String text) {<br>        return wait.until(ExpectedConditions.textToBe(locator, text));<br>    }<br>    <br>    // Wait for URL to contain<br>    public boolean waitForUrlContains(String urlFragment) {<br>        return wait.until(ExpectedConditions.urlContains(urlFragment));<br>    }<br>    <br>    // Wait for element to be invisible<br>    public boolean waitForInvisibility(By locator) {<br>        return wait.until(ExpectedConditions.invisibilityOfElementLocated(locator));<br>    }<br>    <br>    // Wait for alert to be present<br>    public Alert waitForAlert() {<br>        return wait.until(ExpectedConditions.alertIsPresent());<br>    }<br>    <br>    // Custom condition<br>    public void waitForAjaxToComplete() {<br>        wait.until(driver -&gt; {<br>            JavascriptExecutor js = (JavascriptExecutor) driver;<br>            return js.executeScript(&quot;return jQuery.active == 0&quot;).equals(true);<br>        });<br>    }<br>}</pre><h3>Fluent Wait</h3><p>For complex polling scenarios:</p><pre>import org.openqa.selenium.support.ui.FluentWait;</pre><pre>public WebElement waitWithPolling(By locator) {<br>    Wait&lt;WebDriver&gt; fluentWait = new FluentWait&lt;&gt;(driver)<br>        .withTimeout(Duration.ofSeconds(30))<br>        .pollingEvery(Duration.ofMillis(500))<br>        .ignoring(NoSuchElementException.class)<br>        .ignoring(StaleElementReferenceException.class);<br>    <br>    return fluentWait.until(driver -&gt; driver.findElement(locator));<br>}</pre><h3>Implicit Wait (Not Recommended)</h3><p>Sets global timeout for all findElement calls:</p><pre>// Avoid using implicit waits<br>driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));</pre><p><strong>Why avoid?</strong> Implicit waits apply to every element lookup, slowing tests unnecessarily. Use explicit waits instead.</p><h3>Page Object Model (POM)</h3><p>The industry-standard pattern for maintainable automation:</p><pre>import org.openqa.selenium.By;<br>import org.openqa.selenium.WebDriver;<br>import org.openqa.selenium.WebElement;<br>import org.openqa.selenium.support.FindBy;<br>import org.openqa.selenium.support.PageFactory;</pre><pre>public class LoginPage {<br>    private WebDriver driver;<br>    private WebDriverWait wait;<br>    <br>    // Using @FindBy annotations<br>    @FindBy(id = &quot;email&quot;)<br>    private WebElement emailField;<br>    <br>    @FindBy(id = &quot;password&quot;)<br>    private WebElement passwordField;<br>    <br>    @FindBy(css = &quot;button[type=&#39;submit&#39;]&quot;)<br>    private WebElement loginButton;<br>    <br>    @FindBy(css = &quot;.error-message&quot;)<br>    private WebElement errorMessage;<br>    <br>    // Constructor<br>    public LoginPage(WebDriver driver) {<br>        this.driver = driver;<br>        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));<br>        PageFactory.initElements(driver, this);<br>    }<br>    <br>    // Actions<br>    public void enterEmail(String email) {<br>        wait.until(ExpectedConditions.visibilityOf(emailField));<br>        emailField.clear();<br>        emailField.sendKeys(email);<br>    }<br>    <br>    public void enterPassword(String password) {<br>        passwordField.clear();<br>        passwordField.sendKeys(password);<br>    }<br>    <br>    public void clickLogin() {<br>        wait.until(ExpectedConditions.elementToBeClickable(loginButton));<br>        loginButton.click();<br>    }<br>    <br>    // Combined action<br>    public DashboardPage login(String email, String password) {<br>        enterEmail(email);<br>        enterPassword(password);<br>        clickLogin();<br>        return new DashboardPage(driver);<br>    }<br>    <br>    // Verifications<br>    public boolean isErrorDisplayed() {<br>        try {<br>            return errorMessage.isDisplayed();<br>        } catch (NoSuchElementException e) {<br>            return false;<br>        }<br>    }<br>    <br>    public String getErrorMessage() {<br>        wait.until(ExpectedConditions.visibilityOf(errorMessage));<br>        return errorMessage.getText();<br>    }<br>}</pre><h3>Base Page Pattern</h3><p>Centralize common functionality:</p><pre>public class BasePage {<br>    protected WebDriver driver;<br>    protected WebDriverWait wait;<br>    protected JavascriptExecutor js;<br>    <br>    public BasePage(WebDriver driver) {<br>        this.driver = driver;<br>        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));<br>        this.js = (JavascriptExecutor) driver;<br>    }<br>    <br>    protected void click(WebElement element) {<br>        wait.until(ExpectedConditions.elementToBeClickable(element));<br>        element.click();<br>    }<br>    <br>    protected void type(WebElement element, String text) {<br>        wait.until(ExpectedConditions.visibilityOf(element));<br>        element.clear();<br>        element.sendKeys(text);<br>    }<br>    <br>    protected String getText(WebElement element) {<br>        wait.until(ExpectedConditions.visibilityOf(element));<br>        return element.getText();<br>    }<br>    <br>    protected void scrollToElement(WebElement element) {<br>        js.executeScript(&quot;arguments[0].scrollIntoView(true);&quot;, element);<br>    }<br>    <br>    protected void clickWithJS(WebElement element) {<br>        js.executeScript(&quot;arguments[0].click();&quot;, element);<br>    }<br>    <br>    protected boolean isElementPresent(By locator) {<br>        try {<br>            driver.findElement(locator);<br>            return true;<br>        } catch (NoSuchElementException e) {<br>            return false;<br>        }<br>    }<br>    <br>    protected void waitForPageLoad() {<br>        wait.until(driver -&gt; js.executeScript(&quot;return document.readyState&quot;).equals(&quot;complete&quot;));<br>    }<br>}</pre><h3>Handling Different Elements</h3><h3>Dropdowns</h3><pre>import org.openqa.selenium.support.ui.Select;</pre><pre>public void selectDropdownOption(WebElement dropdown, String optionText) {<br>    Select select = new Select(dropdown);<br>    <br>    // By visible text<br>    select.selectByVisibleText(optionText);<br>    <br>    // By value<br>    select.selectByValue(&quot;option_value&quot;);<br>    <br>    // By index<br>    select.selectByIndex(2);<br>    <br>    // Get selected option<br>    String selected = select.getFirstSelectedOption().getText();<br>    <br>    // Get all options<br>    List&lt;WebElement&gt; options = select.getOptions();<br>}</pre><h3>Checkboxes and Radio Buttons</h3><pre>public void handleCheckbox(WebElement checkbox, boolean check) {<br>    if (check &amp;&amp; !checkbox.isSelected()) {<br>        checkbox.click();<br>    } else if (!check &amp;&amp; checkbox.isSelected()) {<br>        checkbox.click();<br>    }<br>}</pre><pre>public void selectRadioButton(List&lt;WebElement&gt; radioButtons, String value) {<br>    for (WebElement radio : radioButtons) {<br>        if (radio.getAttribute(&quot;value&quot;).equals(value)) {<br>            radio.click();<br>            break;<br>        }<br>    }<br>}</pre><h3>File Upload</h3><pre>public void uploadFile(WebElement fileInput, String filePath) {<br>    // Direct file path input (no clicking browse button)<br>    fileInput.sendKeys(new File(filePath).getAbsolutePath());<br>}</pre><h3>Alerts</h3><pre>public void handleAlert(String action) {<br>    Alert alert = wait.until(ExpectedConditions.alertIsPresent());<br>    <br>    switch (action) {<br>        case &quot;accept&quot;:<br>            alert.accept();<br>            break;<br>        case &quot;dismiss&quot;:<br>            alert.dismiss();<br>            break;<br>        case &quot;getText&quot;:<br>            String text = alert.getText();<br>            break;<br>        case &quot;sendKeys&quot;:<br>            alert.sendKeys(&quot;some text&quot;);<br>            alert.accept();<br>            break;<br>    }<br>}</pre><h3>iFrames</h3><pre>public void switchToIframe(WebElement iframe) {<br>    driver.switchTo().frame(iframe);<br>}</pre><pre>public void switchToIframeByIndex(int index) {<br>    driver.switchTo().frame(index);<br>}</pre><pre>public void switchToDefaultContent() {<br>    driver.switchTo().defaultContent();<br>}</pre><h3>Windows and Tabs</h3><pre>public void switchToNewWindow() {<br>    String mainWindow = driver.getWindowHandle();<br>    Set&lt;String&gt; allWindows = driver.getWindowHandles();<br>    <br>    for (String window : allWindows) {<br>        if (!window.equals(mainWindow)) {<br>            driver.switchTo().window(window);<br>            break;<br>        }<br>    }<br>}</pre><pre>public void closeCurrentWindowAndSwitchToMain(String mainWindow) {<br>    driver.close();<br>    driver.switchTo().window(mainWindow);<br>}</pre><h3>JavaScript Execution</h3><p>When standard WebDriver methods fail:</p><pre>public class JavaScriptHelper {<br>    private JavascriptExecutor js;<br>    <br>    public JavaScriptHelper(WebDriver driver) {<br>        this.js = (JavascriptExecutor) driver;<br>    }<br>    <br>    // Click element<br>    public void clickElement(WebElement element) {<br>        js.executeScript(&quot;arguments[0].click();&quot;, element);<br>    }<br>    <br>    // Scroll to element<br>    public void scrollToElement(WebElement element) {<br>        js.executeScript(&quot;arguments[0].scrollIntoView(true);&quot;, element);<br>    }<br>    <br>    // Scroll to bottom<br>    public void scrollToBottom() {<br>        js.executeScript(&quot;window.scrollTo(0, document.body.scrollHeight)&quot;);<br>    }<br>    <br>    // Highlight element (for debugging)<br>    public void highlightElement(WebElement element) {<br>        js.executeScript(&quot;arguments[0].style.border=&#39;3px solid red&#39;&quot;, element);<br>    }<br>    <br>    // Get element attribute<br>    public String getAttribute(WebElement element, String attribute) {<br>        return (String) js.executeScript(<br>            &quot;return arguments[0].getAttribute(&#39;&quot; + attribute + &quot;&#39;);&quot;, element<br>        );<br>    }<br>    <br>    // Remove attribute<br>    public void removeAttribute(WebElement element, String attribute) {<br>        js.executeScript(&quot;arguments[0].removeAttribute(&#39;&quot; + attribute + &quot;&#39;);&quot;, element);<br>    }<br>    <br>    // Enter text<br>    public void enterText(WebElement element, String text) {<br>        js.executeScript(&quot;arguments[0].value=&#39;&quot; + text + &quot;&#39;;&quot;, element);<br>    }<br>    <br>    // Check page load status<br>    public boolean isPageLoaded() {<br>        return js.executeScript(&quot;return document.readyState&quot;).equals(&quot;complete&quot;);<br>    }<br>}</pre><h3>Actions Class: Advanced Interactions</h3><pre>import org.openqa.selenium.interactions.Actions;</pre><pre>public class ActionHelper {<br>    private Actions actions;<br>    <br>    public ActionHelper(WebDriver driver) {<br>        this.actions = new Actions(driver);<br>    }<br>    <br>    // Hover over element<br>    public void hoverOver(WebElement element) {<br>        actions.moveToElement(element).perform();<br>    }<br>    <br>    // Right click<br>    public void rightClick(WebElement element) {<br>        actions.contextClick(element).perform();<br>    }<br>    <br>    // Double click<br>    public void doubleClick(WebElement element) {<br>        actions.doubleClick(element).perform();<br>    }<br>    <br>    // Drag and drop<br>    public void dragAndDrop(WebElement source, WebElement target) {<br>        actions.dragAndDrop(source, target).perform();<br>    }<br>    <br>    // Click and hold<br>    public void clickAndHold(WebElement element) {<br>        actions.clickAndHold(element).perform();<br>    }<br>    <br>    // Release<br>    public void release() {<br>        actions.release().perform();<br>    }<br>    <br>    // Keyboard actions<br>    public void sendKeys(Keys key) {<br>        actions.sendKeys(key).perform();<br>    }<br>    <br>    // Complex action chain<br>    public void complexAction(WebElement element, String text) {<br>        actions<br>            .moveToElement(element)<br>            .click()<br>            .keyDown(Keys.SHIFT)<br>            .sendKeys(text)<br>            .keyUp(Keys.SHIFT)<br>            .perform();<br>    }<br>}</pre><h3>Taking Screenshots</h3><pre>import org.openqa.selenium.OutputType;<br>import org.openqa.selenium.TakesScreenshot;<br>import org.apache.commons.io.FileUtils;</pre><pre>public class ScreenshotHelper {<br>    <br>    public static void takeScreenshot(WebDriver driver, String fileName) {<br>        try {<br>            TakesScreenshot ts = (TakesScreenshot) driver;<br>            File source = ts.getScreenshotAs(OutputType.FILE);<br>            File destination = new File(&quot;./screenshots/&quot; + fileName + &quot;.png&quot;);<br>            FileUtils.copyFile(source, destination);<br>        } catch (IOException e) {<br>            e.printStackTrace();<br>        }<br>    }<br>    <br>    // Screenshot of specific element<br>    public static void takeElementScreenshot(WebElement element, String fileName) {<br>        try {<br>            File source = element.getScreenshotAs(OutputType.FILE);<br>            File destination = new File(&quot;./screenshots/&quot; + fileName + &quot;.png&quot;);<br>            FileUtils.copyFile(source, destination);<br>        } catch (IOException e) {<br>            e.printStackTrace();<br>        }<br>    }<br>}</pre><h3>Handling Stale Element Reference</h3><pre>public WebElement retryFindElement(By locator, int attempts) {<br>    WebElement element = null;<br>    for (int i = 0; i &lt; attempts; i++) {<br>        try {<br>            element = driver.findElement(locator);<br>            return element;<br>        } catch (StaleElementReferenceException e) {<br>            System.out.println(&quot;Stale element, retrying... Attempt &quot; + (i + 1));<br>        }<br>    }<br>    throw new StaleElementReferenceException(&quot;Element is stale after &quot; + attempts + &quot; attempts&quot;);<br>}</pre><pre>public void retryClick(By locator, int attempts) {<br>    for (int i = 0; i &lt; attempts; i++) {<br>        try {<br>            driver.findElement(locator).click();<br>            return;<br>        } catch (StaleElementReferenceException e) {<br>            System.out.println(&quot;Stale element on click, retrying...&quot;);<br>        }<br>    }<br>}</pre><h3>Best Practices</h3><h3>1. Use WebDriver Manager</h3><p>Avoid manual driver management and version issues.</p><h3>2. Implement Proper Waits</h3><p>Always use explicit waits, never Thread.sleep() or implicit waits.</p><h3>3. Page Object Model is Mandatory</h3><p>Encapsulate page logic in page objects for maintainability.</p><h3>4. Prefer CSS Selectors</h3><p>CSS selectors are faster and more readable than XPath.</p><h3>5. Handle Exceptions Gracefully</h3><pre>public boolean safeClick(WebElement element) {<br>    try {<br>        wait.until(ExpectedConditions.elementToBeClickable(element));<br>        element.click();<br>        return true;<br>    } catch (Exception e) {<br>        System.out.println(&quot;Click failed: &quot; + e.getMessage());<br>        return false;<br>    }<br>}</pre><h3>6. Close Resources Properly</h3><pre>@After<br>public void tearDown() {<br>    if (driver != null) {<br>        driver.quit(); // Not close(), use quit()<br>    }<br>}</pre><h3>7. Use Relative Locators (Selenium 4)</h3><pre>import static org.openqa.selenium.support.locators.RelativeLocator.*;</pre><pre>WebElement password = driver.findElement(<br>    with(By.tagName(&quot;input&quot;))<br>    .below(By.id(&quot;email&quot;))<br>);</pre><pre>WebElement cancelButton = driver.findElement(<br>    with(By.tagName(&quot;button&quot;))<br>    .toLeftOf(By.id(&quot;submit&quot;))<br>);</pre><h3>Conclusion</h3><p>Selenium WebDriver remains the gold standard for browser automation despite newer alternatives. Mastering its locator strategies, wait mechanisms, page object patterns, and handling edge cases creates robust, maintainable automation frameworks. The key is understanding when to use standard WebDriver methods versus JavaScript execution, and always prioritizing explicit waits and proper element handling strategies.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8cb8426eeda1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mastering Gherkin: Writing Effective BDD Scenarios]]></title>
            <link>https://medium.com/@imanthasandaumini1/mastering-gherkin-writing-effective-bdd-scenarios-b81e28cad83d?source=rss-3fd07f7d72c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/b81e28cad83d</guid>
            <dc:creator><![CDATA[Sandumini Karunarathne]]></dc:creator>
            <pubDate>Sat, 25 Oct 2025 16:52:45 GMT</pubDate>
            <atom:updated>2025-10-25T17:03:20.816Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/916/1*ev5-_DlzVotuSjrAKANvMA.png" /></figure><p>Gherkin is the domain-specific language that powers Behavior-Driven Development (BDD), enabling teams to write executable specifications in plain English. While it appears simple, writing effective Gherkin requires understanding its core principles and avoiding common pitfalls.</p><h3>Understanding Gherkin’s Core Structure</h3><p>Gherkin uses a structured syntax built around keywords that define test scenarios:</p><pre>Feature: User Authentication<br>  As a registered user<br>  I want to log into the system<br>  So that I can access my account</pre><pre>  Background:<br>    Given the application is running<br>    And the database is seeded with test data</pre><pre>  Scenario: Successful login with valid credentials<br>    Given I am on the login page<br>    When I enter &quot;user@example.com&quot; as email<br>    And I enter &quot;SecurePass123&quot; as password<br>    And I click the login button<br>    Then I should be redirected to the dashboard<br>    And I should see &quot;Welcome back&quot; message</pre><pre>  Scenario: Failed login with invalid password<br>    Given I am on the login page<br>    When I enter &quot;user@example.com&quot; as email<br>    And I enter &quot;wrongpassword&quot; as password<br>    And I click the login button<br>    Then I should see &quot;Invalid credentials&quot; error message<br>    And I should remain on the login page</pre><h3>The Given-When-Then Pattern</h3><p>This pattern represents the fundamental structure of every scenario:</p><ul><li><strong>Given</strong>: Establishes the initial context (preconditions)</li><li><strong>When</strong>: Describes the action or event</li><li><strong>Then</strong>: Specifies the expected outcome</li></ul><p>This maps directly to the Arrange-Act-Assert pattern in unit testing.</p><h3>Background: Reducing Repetition</h3><p>The Background keyword eliminates duplication across scenarios:</p><pre>Feature: Shopping Cart</pre><pre>  Background:<br>    Given I am logged in as &quot;customer@example.com&quot;<br>    And my cart is empty<br>    And the following products are available:<br>      | Product      | Price | Stock |<br>      | Laptop       | 999   | 5     |<br>      | Mouse        | 25    | 20    |<br>      | Keyboard     | 75    | 15    |</pre><pre>  Scenario: Add single item to cart<br>    When I add &quot;Laptop&quot; to cart<br>    Then my cart should contain 1 item<br>    And the cart total should be &quot;$999&quot;</pre><pre>  Scenario: Add multiple items to cart<br>    When I add &quot;Mouse&quot; to cart<br>    And I add &quot;Keyboard&quot; to cart<br>    Then my cart should contain 2 items<br>    And the cart total should be &quot;$100&quot;</pre><p><strong>Important</strong>: Background runs before EVERY scenario in the feature. Keep it lightweight.</p><h3>Scenario Outline: Data-Driven Testing</h3><p>Scenario Outline enables testing multiple data combinations without duplicating scenarios:</p><pre>Scenario Outline: Login validation with different inputs<br>  Given I am on the login page<br>  When I enter &quot;&lt;email&gt;&quot; as email<br>  And I enter &quot;&lt;password&gt;&quot; as password<br>  And I click the login button<br>  Then I should see &quot;&lt;message&gt;&quot;</pre><pre>  Examples:<br>    | email              | password      | message                    |<br>    | valid@example.com  | ValidPass123  | Welcome back               |<br>    | invalid@example    | ValidPass123  | Invalid email format       |<br>    | valid@example.com  | short         | Password too short         |<br>    | valid@example.com  |               | Password is required       |<br>    |                    | ValidPass123  | Email is required          |</pre><p>Each row in the Examples table generates a separate test execution.</p><h3>Multiple Examples Tables</h3><p>You can group related test cases:</p><pre>Scenario Outline: Password strength validation<br>  When I enter &quot;&lt;password&gt;&quot; as new password<br>  Then the strength indicator should show &quot;&lt;strength&gt;&quot;</pre><pre>  Examples: Weak passwords<br>    | password | strength |<br>    | 123456   | Weak     |<br>    | password | Weak     |<br>    | abc123   | Weak     |</pre><pre>  Examples: Strong passwords<br>    | password       | strength |<br>    | P@ssw0rd123!  | Strong   |<br>    | MyS3cur3P@ss  | Strong   |</pre><h3>Data Tables: Handling Complex Data</h3><p>Data tables allow passing structured data to step definitions:</p><pre>Scenario: Create user with complete profile<br>  Given I am on the registration page<br>  When I submit the registration form with:<br>    | Field           | Value                |<br>    | First Name      | John                 |<br>    | Last Name       | Doe                  |<br>    | Email           | john.doe@example.com |<br>    | Phone           | +1234567890          |<br>    | Date of Birth   | 1990-01-15           |<br>    | Address         | 123 Main Street      |<br>    | City            | New York             |<br>    | Postal Code     | 10001                |<br>  Then the user account should be created successfully<br>  And a welcome email should be sent to &quot;john.doe@example.com&quot;</pre><p><strong>Vertical tables</strong> (shown above) work well for single entity creation.</p><p><strong>Horizontal tables</strong> are better for lists:</p><pre>Scenario: Bulk product import<br>  When I import the following products:<br>    | SKU    | Name     | Price | Category    |<br>    | LP001  | Laptop   | 999   | Electronics |<br>    | MS001  | Mouse    | 25    | Accessories |<br>    | KB001  | Keyboard | 75    | Accessories |<br>  Then all 3 products should be created<br>  And they should appear in the product catalog</pre><h3>Tags: Organizing and Filtering Tests</h3><p>Tags enable selective test execution and organization:</p><pre>@smoke @authentication<br>Feature: User Login</pre><pre>  @positive @priority-high<br>  Scenario: Successful login<br>    Given I am on the login page<br>    When I enter valid credentials<br>    Then I should access my dashboard</pre><pre>  @negative @priority-medium<br>  Scenario: Login with invalid credentials<br>    Given I am on the login page<br>    When I enter invalid credentials<br>    Then I should see an error message</pre><pre>  @negative @priority-low @slow<br>  Scenario: Account lockout after failed attempts<br>    Given I am on the login page<br>    When I enter wrong password 5 times<br>    Then my account should be locked<br>    And I should see &quot;Account locked&quot; message</pre><p>Common tag strategies:</p><ul><li><strong>Test type</strong>: @smoke, @regression, @integration</li><li><strong>Priority</strong>: @priority-high, @priority-medium, @priority-low</li><li><strong>Status</strong>: @wip (work in progress), @skip, @manual</li><li><strong>Features</strong>: @authentication, @checkout, @payments</li><li><strong>Performance</strong>: @slow, @fast</li></ul><h3>Doc Strings: Multi-line Text Input</h3><p>For large text inputs, use triple quotes:</p><pre>Scenario: Submit support ticket with detailed description<br>  Given I am logged into the support portal<br>  When I create a new ticket with description:<br>    &quot;&quot;&quot;<br>    I am experiencing intermittent connection issues when<br>    trying to upload files larger than 10MB. The upload<br>    starts normally but fails around 60% completion.<br>    <br>    Steps to reproduce:<br>    1. Navigate to upload page<br>    2. Select file &gt; 10MB<br>    3. Click upload<br>    4. Wait for progress bar to reach ~60%<br>    <br>    Expected: File uploads successfully<br>    Actual: Upload fails with timeout error<br>    &quot;&quot;&quot;<br>  Then the ticket should be created with status &quot;Open&quot;<br>  And support team should be notified</pre><h3>Best Practices for Writing Gherkin</h3><h3>1. Write Declarative, Not Imperative Steps</h3><p><strong>Bad (Imperative)</strong>:</p><pre>Given I open the browser<br>And I navigate to &quot;https://example.com/login&quot;<br>And I locate the email field<br>And I type &quot;user@example.com&quot;<br>And I locate the password field<br>And I type &quot;password123&quot;<br>And I locate the submit button<br>And I click it</pre><p><strong>Good (Declarative)</strong>:</p><pre>Given I am on the login page<br>When I login with email &quot;user@example.com&quot; and password &quot;password123&quot;<br>Then I should be logged in successfully</pre><h3>2. Keep Scenarios Independent</h3><p>Each scenario should be self-contained and executable in any order:</p><p><strong>Bad</strong>:</p><pre>Scenario: Create user account<br>  When I register with email &quot;test@example.com&quot;<br>  Then account should be created</pre><pre>Scenario: Login with new account<br>  # Depends on previous scenario!<br>  When I login with &quot;test@example.com&quot;<br>  Then I should be logged in</pre><p><strong>Good</strong>:</p><pre>Scenario: Login with new account<br>  Given a user exists with email &quot;test@example.com&quot;<br>  When I login with &quot;test@example.com&quot;<br>  Then I should be logged in</pre><h3>3. Use Business Language, Not Technical Implementation</h3><p><strong>Bad</strong>:</p><pre>When I send POST request to &quot;/api/users&quot; with JSON body<br>And the response code is 201<br>Then the database should have new record in users table</pre><p><strong>Good</strong>:</p><pre>When I create a new user account<br>Then the user should be registered successfully<br>And the user should receive a confirmation email</pre><h3>4. One Scenario = One Feature/Behavior</h3><p>Keep scenarios focused on a single behavior:</p><p><strong>Bad</strong>:</p><pre>Scenario: User registration and profile update and password change<br>  Given I register a new account<br>  When I update my profile information<br>  And I change my password<br>  And I add a profile picture<br>  Then everything should work</pre><p><strong>Good</strong>:</p><pre>Scenario: User registration<br>  When I register with valid information<br>  Then my account should be created</pre><pre>Scenario: Update profile information<br>  Given I am logged in<br>  When I update my profile with new information<br>  Then my profile should be updated</pre><pre>Scenario: Change password<br>  Given I am logged in<br>  When I change my password<br>  Then I should be able to login with new password</pre><h3>5. Avoid Conjunctive Steps</h3><p>Don’t use “And” to hide multiple assertions:</p><p><strong>Bad</strong>:</p><pre>Then I should see welcome message and profile picture and notification count</pre><p><strong>Good</strong>:</p><pre>Then I should see &quot;Welcome back&quot; message<br>And I should see my profile picture<br>And I should see notification count</pre><h3>Comments and Documentation</h3><p>Use comments to explain complex scenarios or business rules:</p><pre># Tax calculation varies by state and product category<br># See: https://docs.company.com/tax-rules<br>Scenario: Calculate order total with tax<br>  Given I am in &quot;California&quot;<br>  And my cart contains &quot;Electronics&quot; worth $1000<br>  # CA tax rate for electronics is 9.5%<br>  When I proceed to checkout<br>  Then the tax should be $95.00<br>  And the total should be $1095.00</pre><h3>Language Support</h3><p>Gherkin supports multiple languages:</p><pre># language: es<br>Característica: Autenticación de usuario</pre><pre>  Escenario: Login exitoso<br>    Dado que estoy en la página de login<br>    Cuando ingreso credenciales válidas<br>    Entonces debería ver mi panel de control</pre><p>Common languages: English (en), Spanish (es), French (fr), German (de), Chinese (zh-CN), Japanese (ja).</p><h3>Common Anti-Patterns to Avoid</h3><h3>1. UI-Coupled Steps</h3><pre># Bad<br>When I click the button with id &quot;submit-btn&quot;<br>And I wait 2 seconds<br>Then the div with class &quot;success-msg&quot; should be visible</pre><pre># Good<br>When I submit the registration form<br>Then I should see a success message</pre><h3>2. Overuse of Scenario Outline</h3><pre># Bad - Simple scenarios don&#39;t need outline<br>Scenario Outline: View homepage<br>  When I navigate to homepage<br>  Then I should see &quot;&lt;title&gt;&quot;<br>  <br>  Examples:<br>    | title |<br>    | Home  |</pre><h3>3. Testing Multiple Paths in One Scenario</h3><pre># Bad<br>Scenario: Registration with validation<br>  When I submit empty form<br>  Then I see errors<br>  When I enter valid data<br>  Then registration succeeds<br>  # This should be 2 separate scenarios</pre><h3>Conclusion</h3><p>Gherkin’s power lies in its simplicity and readability. By following these principles, writing declarative steps, maintaining scenario independence, using business language, and leveraging features like Scenario Outline and data tables, you create living documentation that serves as both specification and automated tests.</p><p>The key is remembering that Gherkin scenarios are communication tools first, test automation second. They should be readable by anyone on the team, regardless of technical background.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b81e28cad83d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[CI/CD Tools Deep Dive: Comparing Jenkins, GitHub Actions, GitLab CI, CircleCI, and Azure DevOps]]></title>
            <link>https://medium.com/@imanthasandaumini1/ci-cd-tools-deep-dive-comparing-jenkins-github-actions-gitlab-ci-circleci-and-azure-devops-97b9370dbee7?source=rss-3fd07f7d72c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/97b9370dbee7</guid>
            <dc:creator><![CDATA[Sandumini Karunarathne]]></dc:creator>
            <pubDate>Mon, 25 Aug 2025 18:26:50 GMT</pubDate>
            <atom:updated>2025-08-25T18:26:50.428Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZWAA8F2kgqGK-wXXn6kWEg.jpeg" /></figure><p>I’m learning CI/CD alongside you, and this article will compare five popular CI/CD tools <strong>Jenkins</strong>, <strong>GitHub Actions</strong>, <strong>GitLab CI</strong>, <strong>CircleCI</strong>, and <strong>Azure DevOps</strong> to help you choose the right one for your projects. We’ll explore their configurations, strengths, weaknesses, and real-world use cases, plus how they integrate with DevOps tools like Docker and Terraform. Let’s get started!</p><h3>Why Choose the Right CI/CD Tool?</h3><p>CI/CD tools automate the process of building, testing, and deploying code, saving time and reducing errors. But with so many options, picking the right tool depends on your team’s needs, budget, and tech stack. This article will break down each tool’s features, provide configuration examples, and highlight their role in the DevOps ecosystem, so you can make an informed choice.</p><h3>The Contenders: An Overview</h3><p>Here’s a quick look at the five tools we’ll compare:</p><ol><li><strong>Jenkins</strong>: An open-source, highly customizable CI/CD server with a vast plugin ecosystem.</li><li><strong>GitHub Actions</strong>: A cloud-based CI/CD tool integrated with GitHub, ideal for GitHub-centric workflows.</li><li><strong>GitLab CI</strong>: A built-in CI/CD system within GitLab, offering end-to-end DevOps capabilities.</li><li><strong>CircleCI</strong>: A cloud-native CI/CD tool known for speed, simplicity, and scalability.</li><li><strong>Azure DevOps</strong>: A Microsoft platform for CI/CD, project management, and cloud integration.</li></ol><p>We’ll use the Node.js app from previous articles to illustrate configurations for each tool.</p><h3>1. Jenkins</h3><h3>Overview</h3><p>Jenkins is the granddaddy of CI/CD tools, launched in 2011 as a fork of Hudson. It’s open-source, server-based, and runs on your infrastructure or cloud. Its strength lies in its flexibility and thousands of plugins.</p><h3>Strengths</h3><ul><li><strong>Customizability</strong>: Plugins for nearly every tool (e.g., Docker, Kubernetes, AWS).</li><li><strong>Pipeline as Code</strong>: Use Jenkinsfile for version-controlled pipelines.</li><li><strong>Community</strong>: Large community with extensive documentation.</li></ul><h3>Weaknesses</h3><ul><li><strong>Setup Complexity</strong>: Requires manual server setup and maintenance.</li><li><strong>UI/UX</strong>: Older interface (Blue Ocean helps but isn’t default).</li><li><strong>Learning Curve</strong>: Steeper for beginners compared to cloud-native tools.</li></ul><h3>Configuration Example</h3><p>We built a Jenkins pipeline in the third article. Here’s a simplified Jenkinsfile for our Node.js app, with Docker integration:</p><pre>pipeline {<br>    agent { docker { image &#39;node:16&#39; } }<br>    stages {<br>        stage(&#39;Checkout&#39;) {<br>            steps {<br>                git url: &#39;https://github.com/your-username/my-cicd-app.git&#39;, branch: &#39;main&#39;<br>            }<br>        }<br>        stage(&#39;Build&#39;) {<br>            steps {<br>                sh &#39;npm install&#39;<br>            }<br>        }<br>        stage(&#39;Test&#39;) {<br>            steps {<br>                sh &#39;npm test&#39;<br>            }<br>        }<br>        stage(&#39;Deploy&#39;) {<br>            steps {<br>                sh &#39;echo &quot;Deploying to staging...&quot;&#39;<br>            }<br>        }<br>    }<br>}</pre><h3>Use Case</h3><p>Jenkins shines in enterprises with complex, custom workflows or on-premises requirements. For example, a financial company might use Jenkins to integrate with legacy systems and deploy to private clouds.</p><h3>2. GitHub Actions</h3><h3>Overview</h3><p>GitHub Actions, introduced in 2018, is tightly integrated with GitHub, making it a favorite for open-source and GitHub-based projects. Workflows are defined in YAML files stored in the repository.</p><h3>Strengths</h3><ul><li><strong>GitHub Integration</strong>: Seamless with GitHub repositories and pull requests.</li><li><strong>Ease of Use</strong>: Simple setup for beginners, with a marketplace of pre-built actions.</li><li><strong>Free Tier</strong>: Generous for open-source projects.</li></ul><h3>Weaknesses</h3><ul><li><strong>GitHub-Centric</strong>: Less ideal for non-GitHub workflows.</li><li><strong>Cost for Private Repos</strong>: Can get expensive for large teams.</li><li><strong>Limited Enterprise Features</strong>: Fewer options for on-premises or complex setups.</li></ul><h3>Configuration Example</h3><p>From the second article, here’s a GitHub Actions workflow (.github/workflows/ci-cd.yml):</p><pre>name: CI/CD Pipeline<br>on:<br>  push:<br>    branches: [ main ]<br>jobs:<br>  build-and-test:<br>    runs-on: ubuntu-latest<br>    steps:<br>      - uses: actions/checkout@v3<br>      - uses: actions/setup-node@v3<br>        with:<br>          node-version: &#39;16&#39;<br>      - run: npm install<br>      - run: npm test<br>      - run: echo &quot;Deploying to staging...&quot;</pre><h3>Use Case</h3><p>GitHub Actions is perfect for startups or open-source projects using GitHub. For example, a small team building a web app can quickly set up a pipeline to test and deploy to Vercel.</p><h3>3. GitLab CI</h3><h3>Overview</h3><p>GitLab CI is built into the GitLab platform, offering a unified DevOps experience with source control, CI/CD, and monitoring. Workflows are defined in .gitlab-ci.yml.</p><h3>Strengths</h3><ul><li><strong>All-in-One</strong>: Integrates source control, CI/CD, and project management.</li><li><strong>Scalability</strong>: Supports complex pipelines for microservices or monorepos.</li><li><strong>Free Features</strong>: Generous free tier, including private repositories.</li></ul><h3>Weaknesses</h3><ul><li><strong>GitLab Dependency</strong>: Best for teams using GitLab as their primary platform.</li><li><strong>Learning Curve</strong>: YAML configuration can be complex for advanced setups.</li><li><strong>Self-Hosted Overhead</strong>: Self-hosted GitLab requires maintenance.</li></ul><h3>Configuration Example</h3><p>Here’s a .gitlab-ci.yml for our Node.js app:</p><pre>image: node:16<br>stages:<br>  - build<br>  - test<br>  - deploy<br>build_job:<br>  stage: build<br>  script:<br>    - npm install<br>test_job:<br>  stage: test<br>  script:<br>    - npm test<br>deploy_job:<br>  stage: deploy<br>  script:<br>    - echo &quot;Deploying to staging...&quot;</pre><h3>Use Case</h3><p>GitLab CI is ideal for teams using GitLab for end-to-end DevOps, such as a SaaS company managing microservices with Kubernetes integration.</p><h3>4. CircleCI</h3><h3>Overview</h3><p>CircleCI is a cloud-native CI/CD tool launched in 2011, known for its speed and simplicity. It supports both cloud and self-hosted options, with YAML-based configurations.</p><h3>Strengths</h3><ul><li><strong>Speed</strong>: Optimized for fast builds and parallelization.</li><li><strong>Ease of Use</strong>: Intuitive UI and simple YAML syntax.</li><li><strong>Integration</strong>: Supports Docker, AWS, and other platforms.</li></ul><h3>Weaknesses</h3><ul><li><strong>Cost</strong>: Limited free tier; can be pricey for large teams.</li><li><strong>Less Customizable</strong>: Fewer plugins than Jenkins.</li><li><strong>Cloud Focus</strong>: Self-hosted option is less mature.</li></ul><h3>Configuration Example</h3><p>Here’s a CircleCI config (.circleci/config.yml):</p><pre>version: 2.1<br>jobs:<br>  build-and-test:<br>    docker:<br>      - image: cimg/node:16.20<br>    steps:<br>      - checkout<br>      - run: npm install<br>      - run: npm test<br>      - run: echo &quot;Deploying to staging...&quot;<br>workflows:<br>  build-test-deploy:<br>    jobs:<br>      - build-and-test</pre><h3>Use Case</h3><p>CircleCI is great for startups or teams prioritizing speed and simplicity, like a mobile app developer deploying to AWS.</p><h3>5. Azure DevOps</h3><h3>Overview</h3><p>Azure DevOps, by Microsoft, is a comprehensive platform for CI/CD, project management, and cloud integration, with both cloud and on-premises options.</p><h3>Strengths</h3><ul><li><strong>Enterprise Features</strong>: Robust for large teams, with Azure integration.</li><li><strong>Flexibility</strong>: Supports YAML and classic (GUI-based) pipelines.</li><li><strong>Integration</strong>: Seamless with Azure, GitHub, and other tools.</li></ul><h3>Weaknesses</h3><ul><li><strong>Complexity</strong>: Steeper learning curve for non-Microsoft users.</li><li><strong>Cost</strong>: Can be expensive for non-Azure users.</li><li><strong>Microsoft Focus</strong>: Best for teams in the Microsoft ecosystem.</li></ul><h3>Configuration Example</h3><p>Here’s an Azure pipeline (azure-pipelines.yml):</p><pre>trigger:<br>  - main<br>pool:<br>  vmImage: ubuntu-latest<br>steps:<br>  - task: NodeTool@0<br>    inputs:<br>      versionSpec: &#39;16.x&#39;<br>  - script: npm install<br>    displayName: &#39;Install Dependencies&#39;<br>  - script: npm test<br>    displayName: &#39;Run Tests&#39;<br>  - script: echo Deploying to staging...<br>    displayName: &#39;Deploy to Staging&#39;</pre><h3>Use Case</h3><p>Azure DevOps suits enterprises using Azure, like a retail company deploying .NET apps to Azure cloud services.</p><h3>Comparative Analysis</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/894/1*5XwK5CpbHO8U54Q1Tdl07g.png" /></figure><h3>Integrating with DevOps Tools</h3><p>CI/CD tools don’t work alone — they integrate with other DevOps tools:</p><ul><li><strong>Docker</strong>: All tools support Docker for consistent environments. For example, Jenkins and CircleCI use Docker agents to run builds in containers.</li><li><strong>Kubernetes</strong>: GitLab CI and Azure DevOps have strong Kubernetes integrations for deploying microservices.</li><li><strong>Terraform</strong>: Jenkins and GitLab CI can run Terraform scripts to provision infrastructure, ensuring environment consistency.</li><li><strong>Example</strong>: Extend the Jenkins pipeline to build and push a Docker image:</li></ul><pre>stage(&#39;Build Docker Image&#39;) {<br>    steps {<br>        sh &#39;docker build -t my-cicd-app:latest .&#39;<br>        sh &#39;docker push my-cicd-app:latest&#39;<br>    }<br>}</pre><h3>Common Challenges and Solutions</h3><ol><li><strong>Tool Lock-In</strong>:</li></ol><ul><li><strong>Problem</strong>: Tools like GitHub Actions or GitLab CI tie you to their platforms.</li><li><strong>Solution</strong>: Use portable configurations (e.g., Docker) and export pipelines to other tools.</li></ul><p><strong>2. Cost Management</strong>:</p><ul><li><strong>Problem</strong>: Cloud-based tools can get expensive for large teams.</li><li><strong>Solution</strong>: Optimize pipelines with caching and parallelization, or use self-hosted options like Jenkins.</li></ul><p><strong>3. Complex Configurations</strong>:</p><ul><li><strong>Problem</strong>: YAML or Groovy syntax errors break pipelines.</li><li><strong>Solution</strong>: Use linters (e.g., yamllint for GitLab CI) and test pipelines locally.</li></ul><h3>Real-World Case Studies</h3><ul><li><strong>Jenkins</strong>: A bank uses Jenkins to automate builds for a legacy Java app, integrating with on-premises servers and custom plugins.</li><li><strong>GitHub Actions</strong>: An open-source project uses GitHub Actions to test and deploy a Python app to AWS Lambda, leveraging the free tier.</li><li><strong>GitLab CI</strong>: A SaaS startup uses GitLab CI for microservices, deploying to Kubernetes with GitLab’s Auto DevOps.</li><li><strong>CircleCI</strong>: A mobile app team uses CircleCI to build and test iOS/Android apps, deploying to AWS S3.</li><li><strong>Azure DevOps</strong>: An enterprise uses Azure DevOps to manage .NET app deployments across Azure cloud regions.</li></ul><h3>What We’ve Learned</h3><p>In this article, we:</p><ul><li>Compared five CI/CD tools: Jenkins, GitHub Actions, GitLab CI, CircleCI, and Azure DevOps.</li><li>Provided configuration examples for our Node.js app.</li><li>Highlighted strengths, weaknesses, and use cases.</li><li>Explored integrations with Docker, Kubernetes, and Terraform.</li></ul><p>Choosing a tool depends on your team’s needs: Jenkins for flexibility, GitHub Actions for simplicity, GitLab CI for end-to-end DevOps, CircleCI for speed, and Azure DevOps for Microsoft ecosystems.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=97b9370dbee7" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Continuous Delivery and Deployment: Automating Your Path to Production]]></title>
            <link>https://medium.com/@imanthasandaumini1/continuous-delivery-and-deployment-automating-your-path-to-production-5c9c5f69119d?source=rss-3fd07f7d72c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/5c9c5f69119d</guid>
            <dc:creator><![CDATA[Sandumini Karunarathne]]></dc:creator>
            <pubDate>Wed, 20 Aug 2025 13:00:05 GMT</pubDate>
            <atom:updated>2025-08-20T13:00:05.859Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RVgtVrFBVIiRwD2NuRN18A.jpeg" /></figure><p>Welcome back to our series on <strong>CI/CD pipelines</strong>! In the previous articles, we covered the basics of CI/CD, built pipelines with GitHub Actions and Jenkins, and explored core Continuous Integration (CI) concepts. Now, it’s time to take the next step: <strong>Continuous Delivery and Deployment</strong>.</p><h3>What Are Continuous Delivery and Deployment?</h3><p><strong>Continuous Delivery (CD)</strong> and <strong>Continuous Deployment</strong> extend Continuous Integration by automating the release process, ensuring code is always ready to deploy or even deployed automatically to production.</p><ul><li><strong>Continuous Delivery</strong>: Every change that passes the CI pipeline (build and tests) is automatically deployed to a staging or production-like environment. However, deploying to production requires manual approval, giving teams control over the release process.</li><li><strong>Continuous Deployment</strong>: Takes automation further by deploying every passing change directly to production without manual intervention. This requires robust testing and confidence in the pipeline.</li></ul><p>Think of CI as ensuring your code is always <em>buildable</em> and <em>testable</em>, while CD ensures it’s <em>deployable</em> and Continuous Deployment makes it <em>deployed</em>.</p><h3>Why Continuous Delivery and Deployment Matter</h3><p>CD streamlines the release process, offering:</p><ul><li><strong>Faster Releases</strong>: Automated deployments reduce manual effort, enabling frequent releases.</li><li><strong>Reduced Risk</strong>: Small, incremental changes are easier to test and roll back than large updates.</li><li><strong>Improved Reliability</strong>: Consistent deployment processes minimize human errors.</li><li><strong>Customer Satisfaction</strong>: Faster delivery of features and bug fixes keeps users happy.</li></ul><p>For teams, CD means less time spent on deployment logistics. For businesses, it translates to a competitive edge through rapid, reliable software delivery.</p><h3>Key Components of a CD Pipeline</h3><p>A Continuous Delivery pipeline builds on the CI pipeline (source control, build, test) by adding deployment stages. Using the Node.js app from our previous articles, a CD pipeline typically includes:</p><ol><li><strong>Source Control</strong>: Code is pushed to a GitHub repository.</li><li><strong>Build</strong>: Dependencies are installed (e.g., npm install).</li><li><strong>Test</strong>: Automated tests (unit, integration) ensure code quality.</li><li><strong>Deploy to Staging</strong>: Code is deployed to a staging environment for further testing.</li><li><strong>Deploy to Production</strong>: Code is deployed to production (manually for Continuous Delivery, automatically for Continuous Deployment).</li></ol><p>Let’s see how this works in practice.</p><h3>Hands-On: Extending the Jenkins Pipeline for Continuous Delivery</h3><p>We’ll extend the Jenkins pipeline from the third article to include deployment to a staging environment using Heroku (a cloud platform). For Continuous Deployment, we’ll discuss how to automate the production step.</p><h3>Step 1: Set Up the Node.js App</h3><p>If you followed the previous articles, you have a Node.js app with Express, Jest, and Supertest in a GitHub repository (my-cicd-app). If not, refer to the third article to set it up.</p><h3>Step 2: Configure Heroku for Deployment</h3><ol><li><strong>Create a Heroku App</strong>:</li></ol><ul><li>Sign up at heroku.com.</li><li>Create a new app (e.g., my-cicd-staging) via the Heroku dashboard or CLI:</li></ul><pre>heroku create my-cicd-staging</pre><ul><li>Note the app’s Git URL (e.g., <a href="https://git.heroku.com/my-cicd-staging.git).">https://git.heroku.com/my-cicd-staging.git).</a></li></ul><p><strong>2. Add Heroku API Key to Jenkins</strong>:</p><ul><li>In Jenkins, go to <strong>Manage Jenkins</strong> &gt; <strong>Manage Credentials</strong> &gt; <strong>Global Credentials</strong> &gt; <strong>Add Credentials</strong>.</li><li>Add a “Secret text” credential with your Heroku API key (found in your Heroku account settings) and ID heroku-api-key.</li></ul><h3>Step 3: Update the Jenkinsfile</h3><p>Update the Jenkinsfile in your repository to include staging deployment:</p><pre>pipeline {<br>    agent any<br>    tools {<br>        nodejs &#39;Node16&#39;<br>    }<br>    stages {<br>        stage(&#39;Checkout&#39;) {<br>            steps {<br>                git url: &#39;https://github.com/your-username/my-cicd-app.git&#39;, branch: &#39;main&#39;<br>            }<br>        }<br>        stage(&#39;Install Dependencies&#39;) {<br>            steps {<br>                sh &#39;npm install&#39;<br>            }<br>        }<br>        stage(&#39;Run Tests&#39;) {<br>            steps {<br>                sh &#39;npm test&#39;<br>            }<br>        }<br>        stage(&#39;Deploy to Staging&#39;) {<br>            steps {<br>                withCredentials([string(credentialsId: &#39;heroku-api-key&#39;, variable: &#39;HEROKU_API_KEY&#39;)]) {<br>                    sh &#39;&#39;&#39;<br>                        git remote add heroku https://git.heroku.com/my-cicd-staging.git<br>                        git push heroku main<br>                    &#39;&#39;&#39;<br>                }<br>            }<br>        }<br>    }<br>    post {<br>        always {<br>            echo &#39;Pipeline completed!&#39;<br>        }<br>        success {<br>            echo &#39;Pipeline succeeded! App deployed to staging.&#39;<br>        }<br>        failure {<br>            echo &#39;Pipeline failed!&#39;<br>        }<br>    }<br>}</pre><p><strong>Explanation</strong>:</p><ul><li><strong>Deploy to Staging</strong>: Uses Heroku’s Git-based deployment to push the code to the my-cicd-staging app.</li><li><strong>withCredentials</strong>: Securely injects the Heroku API key to authenticate the deployment.</li><li><strong>Post Actions</strong>: Notifies the team of deployment success or failure.</li></ul><h3>Step 4: Commit and Run</h3><pre>git add Jenkinsfile<br>git commit -m &quot;Add staging deployment to Jenkins pipeline&quot;<br>git push origin main</pre><p>In Jenkins, trigger the pipeline via <strong>Build Now</strong>. Check the <strong>Console Output</strong> to confirm the app deploys to Heroku. Visit your Heroku app’s URL (e.g., <a href="https://my-cicd-staging.herokuapp.com)">https://my-cicd-staging.herokuapp.com)</a> to see “Hello, Jenkins CI/CD!”.</p><h3>Step 5: Continuous Deployment (Optional)</h3><p>For Continuous Deployment, add a production deployment stage that runs automatically after staging. <strong>Caution</strong>: This requires robust testing to avoid deploying broken code.</p><pre>stage(&#39;Deploy to Production&#39;) {<br>    when {<br>        branch &#39;main&#39; // Only run for main branch<br>    }<br>    steps {<br>        withCredentials([string(credentialsId: &#39;heroku-api-key&#39;, variable: &#39;HEROKU_API_KEY&#39;)]) {<br>            sh &#39;&#39;&#39;<br>                git remote add heroku-prod https://git.heroku.com/my-cicd-prod.git<br>                git push heroku-prod main<br>            &#39;&#39;&#39;<br>        }<br>    }<br>}</pre><p>Add this stage after Deploy to Staging. Create a separate Heroku app (my-cicd-prod) for production. This setup ensures all passing changes go to production automatically.</p><h3>Rollback Strategies</h3><p>Deployments can fail, so rollback strategies are critical:</p><ul><li><strong>Git Revert</strong>: Revert the last commit if a deployment introduces bugs.</li></ul><pre>git revert HEAD<br>git push heroku main</pre><p><strong>Heroku Rollback</strong>: Roll back to a previous release:</p><pre>heroku rollback --app my-cicd-staging</pre><ul><li><strong>Best Practice</strong>: Store build artifacts (e.g., via Jenkins’ archiveArtifacts) to redeploy a known-good version.</li></ul><h3>Blue-Green Deployments</h3><p>To minimize downtime and risk, use <strong>blue-green deployments</strong>:</p><ul><li><strong>Blue Environment</strong>: The current production app (e.g., my-cicd-prod).</li><li><strong>Green Environment</strong>: A new version deployed alongside blue (e.g., my-cicd-prod-green).</li><li><strong>Process</strong>: Test the green environment, then switch traffic from blue to green (e.g., update DNS or Heroku routing).</li><li><strong>Example</strong>: Deploy to my-cicd-prod-green, test it, then promote it to production using Heroku’s pipeline feature.</li></ul><p><strong>Jenkinsfile Addition</strong>:</p><pre>stage(&#39;Deploy to Green&#39;) {<br>    steps {<br>        withCredentials([string(credentialsId: &#39;heroku-api-key&#39;, variable: &#39;HEROKU_API_KEY&#39;)]) {<br>            sh &#39;&#39;&#39;<br>                git remote add heroku-green https://git.heroku.com/my-cicd-prod-green.git<br>                git push heroku-green main<br>            &#39;&#39;&#39;<br>        }<br>    }<br>}<br>stage(&#39;Promote to Production&#39;) {<br>    steps {<br>        sh &#39;heroku pipelines:promote --app my-cicd-prod-green&#39;<br>    }<br>}</pre><h3>Common Challenges and Solutions</h3><p>CD introduces new challenges beyond CI:</p><ol><li><strong>Environment Consistency</strong>:</li></ol><ul><li><strong>Problem</strong>: Differences between development, staging, and production cause failures.</li><li><strong>Solution</strong>: Use <strong>infrastructure-as-code (IaC)</strong> tools like Terraform or Docker to standardize environments. For example, deploy the Node.js app in a Docker container:</li></ul><pre>FROM node:16<br>WORKDIR /app<br>COPY . .<br>RUN npm install<br>CMD [&quot;npm&quot;, &quot;start&quot;]</pre><p><strong>2. Deployment Failures</strong>:</p><ul><li><strong>Problem</strong>: A bad deployment breaks the app.</li><li><strong>Solution</strong>: Implement rollback strategies and robust testing (e.g., end-to-end tests in staging).</li></ul><p><strong>3. Manual Approval Overload</strong>:</p><ul><li><strong>Problem</strong>: Continuous Delivery requires manual approvals, slowing releases.</li><li><strong>Solution</strong>: Use feature flags to toggle new features without redeploying, or move to Continuous Deployment with strong tests.</li></ul><h3>Interconnections: CD in the DevOps Ecosystem</h3><p>CD connects to other practices:</p><ul><li><strong>Infrastructure-as-Code (IaC)</strong>: Tools like Terraform or Docker ensure consistent environments, as shown in the Docker example above.</li><li><strong>Containerization</strong>: Docker and Kubernetes simplify deployments by packaging apps with dependencies, which we’ll explore in later articles.</li><li><strong>Testing</strong>: CD relies on CI’s automated tests to ensure deployable code.</li><li><strong>Monitoring</strong>: Post-deployment monitoring (e.g., with Prometheus) catches issues in production, covered in a future article.</li></ul><h3>What We’ve Learned</h3><p>In this article, we:</p><ul><li>Defined Continuous Delivery and Continuous Deployment.</li><li>Extended our Jenkins pipeline to deploy to a Heroku staging environment.</li><li>Explored rollback strategies and blue-green deployments.</li><li>Addressed challenges like environment consistency and deployment failures.</li></ul><p>This pipeline builds on CI to automate the path to production, making releases faster and safer.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5c9c5f69119d" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Core CI Concepts and Best Practices: Building a Solid Foundation for CI/CD]]></title>
            <link>https://medium.com/@imanthasandaumini1/core-ci-concepts-and-best-practices-building-a-solid-foundation-for-ci-cd-67bd5c339c0e?source=rss-3fd07f7d72c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/67bd5c339c0e</guid>
            <dc:creator><![CDATA[Sandumini Karunarathne]]></dc:creator>
            <pubDate>Sat, 09 Aug 2025 09:01:35 GMT</pubDate>
            <atom:updated>2025-08-09T09:01:35.015Z</atom:updated>
            <content:encoded><![CDATA[<p>Continuous Integration is all about integrating code changes frequently, automating builds, and catching issues early. By the end of this article, you’ll understand CI principles, how to write effective tests, manage build artifacts, and avoid pitfalls like flaky tests and merge conflicts. Let’s dive in and make CI less intimidating and more actionable!</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*coPngeDrilvpHCfS4I7WRQ.png" /></figure><h3>What Is Continuous Integration?</h3><p><strong>Continuous Integration (CI)</strong> is a development practice where developers frequently integrate their code changes into a shared repository, often multiple times a day. Each integration triggers an automated build and test process to verify that the new code doesn’t break the application. The goal? Catch issues early, improve code quality, and keep the codebase in a deployable state.</p><p>CI is built on three core principles:</p><ol><li><strong>Frequent Commits</strong>: Developers commit small, incremental changes to avoid “integration hell.”</li><li><strong>Automated Builds</strong>: Every commit triggers a build to compile or package the code.</li><li><strong>Automated Testing</strong>: Tests run automatically to ensure the code works as expected.</li></ol><p>For example, imagine a team working on a Node.js web app. Each developer pushes code to a GitHub repository, triggering a pipeline (like the ones we built in previous articles) that builds the app and runs tests. If anything fails, the team gets immediate feedback to fix it.</p><h3>Why CI Matters</h3><p>CI is the foundation of modern software development because it:</p><ul><li><strong>Reduces Bugs</strong>: Early testing catches issues before they reach production.</li><li><strong>Speeds Up Development</strong>: Frequent integration prevents long, painful merge sessions.</li><li><strong>Improves Collaboration</strong>: Teams work on a unified codebase, reducing silos.</li><li><strong>Enables Faster Releases</strong>: A stable codebase is always ready for deployment.</li></ul><p>As a beginner, I’ve learned that CI is not just about tools, it’s a mindset that encourages discipline and automation.</p><h3>Core CI Concepts</h3><p>Let’s explore the key components of CI and how they work together.</p><h3>1. Frequent Commits and Small Changes</h3><p>Committing small, focused changes (e.g., a single feature or bug fix) makes it easier to:</p><ul><li>Identify the source of issues when tests fail.</li><li>Review code effectively during pull requests.</li><li>Roll back problematic changes without major disruptions.</li></ul><p><strong>Best Practice</strong>: Commit at least daily, and break large features into smaller, testable chunks. Use meaningful commit messages (e.g., “Add user authentication endpoint”) to improve traceability.</p><h3>2. Automated Builds</h3><p>A CI pipeline automatically builds the application after each commit. For our Node.js app from previous articles, this means:</p><ul><li>Installing dependencies (npm install).</li><li>Compiling code (if needed, e.g., TypeScript).</li><li>Packaging the app (e.g., creating a Docker image).</li></ul><p><strong>Best Practice</strong>: Ensure builds are fast and reproducible. Use dependency caching (as shown in the GitHub Actions article) to speed up builds, and lock dependency versions (e.g., with package-lock.json) for consistency.</p><h3>3. Automated Testing</h3><p>Tests are the backbone of CI. They verify that code changes don’t introduce bugs. Common test types include:</p><ul><li><strong>Unit Tests</strong>: Test individual functions or components (e.g., a function that calculates a total).</li><li><strong>Integration Tests</strong>: Test interactions between components (e.g., API endpoints).</li><li><strong>End-to-End Tests</strong>: Test the entire application flow (e.g., user login to logout).</li></ul><p>For our Node.js app, we used Jest and Supertest to test the / endpoint. Here’s an example of a unit test for a utility function:</p><pre>// utils.js<br>function add(a, b) {<br>  return a + b;<br>}<br>module.exports = { add };<br><br>// test/utils.test.js<br>const { add } = require(&#39;../utils&#39;);<br><br>describe(&#39;add function&#39;, () =&gt; {<br>  it(&#39;should add two numbers correctly&#39;, () =&gt; {<br>    expect(add(2, 3)).toBe(5);<br>    expect(add(-1, 1)).toBe(0);<br>  });<br>});</pre><p><strong>Best Practice</strong>: Write tests that are:</p><ul><li><strong>Isolated</strong>: Don’t rely on external systems (e.g., databases).</li><li><strong>Fast</strong>: Aim for tests that run in milliseconds.</li><li><strong>Reliable</strong>: Avoid flaky tests (more on this later).</li></ul><h3>4. Build Artifacts</h3><p>A build artifact is the output of a build process, like a compiled binary, JAR file, or Docker image. Artifacts are stored for later use (e.g., deployment).</p><p><strong>Best Practice</strong>: Archive artifacts in your CI tool (e.g., Jenkins’ archiveArtifacts or GitHub Actions’ upload-artifact). For our Node.js app, you might archive the node_modules folder or a bundled app. Example in Jenkins:</p><pre>stage(&#39;Archive Artifacts&#39;) {<br>    steps {<br>        archiveArtifacts artifacts: &#39;package.json, node_modules/**&#39;, fingerprint: true<br>    }<br>}</pre><h3>5. Immediate Feedback</h3><p>CI provides quick feedback to developers via email, Slack, or the CI tool’s dashboard. If a build or test fails, the team knows immediately.</p><p><strong>Best Practice</strong>: Configure notifications (e.g., Jenkins’ Email Extension Plugin or GitHub Actions’ Slack action) to alert the team on failures.</p><h3>Common CI Challenges and Solutions</h3><p>CI isn’t without hurdles. Here are three common issues and how to tackle them:</p><h3>1. Flaky Tests</h3><p>Flaky tests pass or fail inconsistently, eroding trust in the pipeline. Causes include:</p><ul><li>Timing issues (e.g., tests depending on network latency).</li><li>Shared state (e.g., tests modifying a shared database).</li></ul><p><strong>Solution</strong>:</p><ul><li>Mock external dependencies (e.g., use nock for API calls).</li><li>Ensure tests are independent and clean up state (e.g., reset databases between tests).</li><li>Rerun flaky tests automatically (e.g., Jest’s — runInBand or retry plugins).</li></ul><h3>2. Long Build Times</h3><p>Slow builds frustrate developers and delay feedback. For our Node.js app, installing dependencies can be a bottleneck.</p><p><strong>Solution</strong>:</p><ul><li>Cache dependencies (e.g., Jenkins’ shared volume or GitHub Actions’ actions/cache).</li><li>Parallelize test suites using tools like Jest’s — maxWorkers.</li><li>Split large pipelines into smaller jobs.</li></ul><p>Example in GitHub Actions:</p><pre>- name: Cache dependencies<br>  uses: actions/cache@v3<br>  with:<br>    path: ~/.npm<br>    key: ${{ runner.os }}-node-${{ hashFiles(&#39;**/package-lock.json&#39;) }}</pre><h3>3. Merge Conflicts</h3><p>Frequent commits can lead to merge conflicts, especially in large teams.</p><p><strong>Solution</strong>:</p><ul><li>Use <strong>pull requests (PRs)</strong> with code reviews to catch conflicts early.</li><li>Enable branch protection in GitHub to require passing CI checks before merging.</li><li>Rebase or merge regularly to keep branches in sync with main.</li></ul><h3>Interconnections: CI in the DevOps Ecosystem</h3><p>CI doesn’t exist in isolation — it’s tightly linked to other practices:</p><ul><li><strong>Version Control</strong>: CI relies on tools like Git to manage code changes. Frequent commits and PRs ensure a healthy codebase.</li><li><strong>Code Reviews</strong>: CI pipelines run tests on PRs, providing confidence for reviewers. For example, a Jenkins pipeline can comment on a GitHub PR with test results.</li><li><strong>Continuous Delivery</strong>: CI ensures the codebase is always deployable, enabling Continuous Delivery (CD), which we’ll explore in the next article.</li></ul><h3>Best Practices for CI Success</h3><p>To wrap up, here’s a checklist of CI best practices:</p><ol><li><strong>Commit Early, Commit Often</strong>: Small, frequent commits reduce integration pain.</li><li><strong>Automate Everything</strong>: Builds, tests, and notifications should run without manual intervention.</li><li><strong>Prioritize Test Quality</strong>: Write fast, reliable, and isolated tests.</li><li><strong>Keep Builds Fast</strong>: Use caching and parallelization to minimize wait times.</li><li><strong>Monitor and Improve</strong>: Track build success rates and test coverage to identify weak spots.</li></ol><p>For our Node.js app, this means committing small changes, running Jest tests automatically, caching node_modules, and reviewing PRs with CI feedback.</p><h3>What’s Next?</h3><p>In the next article, we’ll explore <strong>Continuous Delivery and Deployment</strong>, building on our CI foundation to automate deployments to staging and production. We’ll cover rollback strategies, blue-green deployments, and environment consistency. Future articles will dive into advanced topics like microservices CI/CD and DevSecOps.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=67bd5c339c0e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Setting Up a Basic CI/CD Pipeline with Jenkins]]></title>
            <link>https://medium.com/@imanthasandaumini1/setting-up-a-basic-ci-cd-pipeline-with-jenkins-a85a4855195f?source=rss-3fd07f7d72c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/a85a4855195f</guid>
            <dc:creator><![CDATA[Sandumini Karunarathne]]></dc:creator>
            <pubDate>Sat, 19 Jul 2025 11:28:42 GMT</pubDate>
            <atom:updated>2025-07-19T11:28:42.472Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/316/1*kGfaWtYjRk47Fzjqw7sheQ.png" /></figure><p>we built a pipeline using GitHub Actions for a simple Node.js web app. Now, let’s dive into <strong>Jenkins</strong>, one of the most popular open-source tools for CI/CD. By the end of this article, you’ll have a working Jenkins pipeline that integrates with a GitHub repository, builds a Node.js app, runs tests, and simulates deployment. Whether you’re new to Jenkins or looking to expand your DevOps toolkit, this guide will make the process approachable and practical. Let’s get started!</p><h3>Why Jenkins?</h3><p>Jenkins is a powerful, flexible CI/CD server that’s been a staple in the DevOps world since 2011. Unlike GitHub Actions, which is tightly integrated with GitHub, Jenkins is a standalone tool that can be customized for almost any workflow. Its key strengths include:</p><ul><li><strong>Extensibility</strong>: Thousands of plugins for integrating with tools like Git, Docker, and AWS.</li><li><strong>Pipeline as Code</strong>: Define pipelines using a Jenkinsfile for version-controlled workflows.</li><li><strong>Community Support</strong>: A large community and extensive documentation.</li></ul><p>However, Jenkins can be more complex to set up than cloud-based tools like GitHub Actions, so we’ll keep things simple and focus on the essentials.</p><h3>What We’ll Build</h3><p>We’ll create a Jenkins pipeline for the same <strong>Node.js web application</strong> we used in the second article. The pipeline will:</p><ol><li>Trigger on code pushes to a GitHub repository.</li><li>Build the application by installing dependencies.</li><li>Run automated tests to verify the code.</li><li>Simulate deployment to a staging environment.</li></ol><p>If you followed the second article, you can reuse the same Node.js app. If not, we’ll recap the setup briefly.</p><h3>Prerequisites</h3><p>Before we begin, you’ll need:</p><ul><li>A <strong>GitHub repository</strong> with a Node.js app (we’ll reuse or create one).</li><li>A <strong>Jenkins server</strong> (we’ll set one up locally or use a cloud instance).</li><li><strong>Docker</strong> (optional, for running Jenkins locally).</li><li>Basic knowledge of Git and JavaScript (helpful but not required).</li></ul><h3>Step 1: Set Up the Node.js Application</h3><p>If you followed the second article, you already have a GitHub repository with a Node.js app. If not, here’s a quick setup:</p><ol><li><strong>Create a GitHub repository</strong> (e.g., my-cicd-app) and clone it:</li></ol><pre>git clone https://github.com/your-username/my-cicd-app.git<br>cd my-cicd-app</pre><p><strong>2. Set up the Node.js app</strong>:</p><ul><li>Initialize a Node.js project:</li></ul><pre>npm init -y</pre><ul><li>Install <strong>Express</strong>:</li></ul><pre>npm install express</pre><ul><li>Create app.js:</li></ul><pre>const express = require(&#39;express&#39;);<br>const app = express();<br><br>app.get(&#39;/&#39;, (req, res) =&gt; {<br>  res.send(&#39;Hello, Jenkins CI/CD!&#39;);<br>});<br><br>const port = process.env.PORT || 3000;<br>app.listen(port, () =&gt; {<br>  console.log(`Server running on port ${port}`);<br>});</pre><ul><li>Install <strong>Jest</strong> and <strong>Supertest</strong> for testing:</li></ul><pre>npm install --save-dev jest supertest</pre><ul><li>Create test/app.test.js:</li></ul><pre>const request = require(&#39;supertest&#39;);<br>const express = require(&#39;express&#39;);<br>const app = express();<br>app.get(&#39;/&#39;, (req, res) =&gt; {<br>  res.send(&#39;Hello, Jenkins CI/CD!&#39;);<br>});<br><br>describe(&#39;GET /&#39;, () =&gt; {<br>  it(&#39;should return Hello, Jenkins CI/CD!&#39;, async () =&gt; {<br>    const res = await request(app).get(&#39;/&#39;);<br>    expect(res.statusCode).toBe(200);<br>    expect(res.text).toBe(&#39;Hello, Jenkins CI/CD!&#39;);<br>  });<br>});</pre><ul><li>Update package.json with test script:</li></ul><pre>&quot;scripts&quot;: {<br>  &quot;start&quot;: &quot;node app.js&quot;,<br>  &quot;test&quot;: &quot;jest&quot;<br>}</pre><p><strong>3. Push to GitHub</strong>:</p><pre>git add .<br>git commit -m &quot;Initial Node.js app setup for Jenkins&quot;<br>git push origin main</pre><h3>Step 2: Set Up Jenkins</h3><p>To run Jenkins, you can install it locally, use a cloud service, or run it via Docker. For simplicity, we’ll use Docker.</p><ol><li><strong>Install Docker</strong> (if not already installed). Follow instructions at [</li></ol><p>docker.com](https://www.docker.com).</p><p>2. <strong>Run Jenkins in a Docker container</strong>:</p><pre>docker run -d -p 8080:8080 -p 50000:50000 --name jenkins -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts</pre><ul><li>This starts Jenkins on <a href="http://localhost:8080">http://localhost:8080</a> and persists data in a jenkins_home volume.</li></ul><p><strong>3. Access Jenkins</strong>:</p><ul><li>Open <a href="http://localhost:8080">http://localhost:8080</a> in your browser.</li><li>Retrieve the initial admin password:</li></ul><pre>docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword</pre><p>Follow the setup wizard to install recommended plugins and create an admin user.</p><p><strong>4. Install Node.js Plugin</strong>:</p><ul><li>Go to <strong>Manage Jenkins</strong> &gt; <strong>Manage Plugins</strong> &gt; <strong>Available</strong>.</li><li>Search for “NodeJS Plugin” and install it (this provides Node.js support for our pipeline).</li></ul><h3>Step 3: Create a Jenkins Pipeline</h3><p>Jenkins pipelines can be defined in a Jenkinsfile, which we’ll store in our repository for version control.</p><ol><li><strong>Create a </strong>Jenkinsfile in your repository’s root:</li></ol><pre>pipeline {<br>    agent any<br>    tools {<br>        nodejs &#39;Node16&#39; // Use Node.js version configured in Jenkins<br>    }<br>    stages {<br>        stage(&#39;Checkout&#39;) {<br>            steps {<br>                git url: &#39;https://github.com/your-username/my-cicd-app.git&#39;, branch: &#39;main&#39;<br>            }<br>        }<br>        stage(&#39;Install Dependencies&#39;) {<br>            steps {<br>                sh &#39;npm install&#39;<br>            }<br>        }<br>        stage(&#39;Run Tests&#39;) {<br>            steps {<br>                sh &#39;npm test&#39;<br>            }<br>        }<br>        stage(&#39;Deploy to Staging&#39;) {<br>            steps {<br>                sh &#39;echo &quot;Deploying to staging environment...&quot;&#39;<br>                // Add real deployment commands here (e.g., to Heroku)<br>            }<br>        }<br>    }<br>    post {<br>        always {<br>            echo &#39;Pipeline completed!&#39;<br>        }<br>        success {<br>            echo &#39;Pipeline succeeded!&#39;<br>        }<br>        failure {<br>            echo &#39;Pipeline failed!&#39;<br>        }<br>    }<br>}</pre><p>Let’s break down the Jenkinsfile:</p><ul><li>agent any: Runs the pipeline on any available Jenkins agent.</li><li>tools: Specifies Node.js version (configured in Jenkins).</li><li>stages: Defines pipeline steps: checkout code, install dependencies, run tests, and deploy.</li><li>post: Executes actions after the pipeline (e.g., notifications).</li></ul><p><strong>2. Push the </strong>Jenkinsfile<strong> to GitHub</strong>:</p><pre>git add Jenkinsfile<br>git commit -m &quot;Add Jenkinsfile for CI/CD pipeline&quot;<br>git push origin main</pre><p><strong>3. Configure Jenkins Pipeline</strong>:</p><ul><li>In Jenkins, click <strong>New Item</strong>, name it (e.g., My-CICD-Pipeline), and select <strong>Pipeline</strong>.</li><li>Under <strong>Pipeline</strong>, choose <strong>Pipeline script from SCM</strong>.</li><li>Set <strong>SCM</strong> to <strong>Git</strong>, enter your repository URL (https://github.com/your-username/my-cicd-app.git), and set <strong>Branch</strong> to main.</li><li>Set <strong>Script Path</strong> to Jenkinsfile.</li><li>Save and click <strong>Build Now</strong> to run the pipeline.</li></ul><p><strong>4. View Pipeline Results</strong>:</p><ul><li>Go to the pipeline’s page in Jenkins and check the <strong>Console Output</strong> for build logs.</li><li>If all steps pass, you’ll see the “Pipeline succeeded!” message.</li></ul><h3>Step 4: Simulate Deployment</h3><p>For learning purposes, our deployment stage simply prints a message. In a real project, you’d add commands to deploy to a platform like Heroku or AWS. For example, to deploy to Heroku:</p><ol><li>Install the <strong>Heroku CLI</strong> plugin in Jenkins.</li><li>Add Heroku credentials in <strong>Manage Jenkins</strong> &gt; <strong>Manage Credentials</strong>.</li><li>Update the Deploy to Staging stage:</li></ol><pre>stage(&#39;Deploy to Staging&#39;) {<br>    steps {<br>        withCredentials([string(credentialsId: &#39;heroku-api-key&#39;, variable: &#39;HEROKU_API_KEY&#39;)]) {<br>            sh &#39;heroku git:remote -a your-app-name&#39;<br>            sh &#39;git push heroku main&#39;<br>        }<br>    }<br>}</pre><h3>Common Pitfalls and How to Avoid Them</h3><p>Jenkins can be tricky for beginners. Here are common issues and solutions:</p><ol><li><strong>Plugin Dependencies</strong>: Missing plugins (e.g., NodeJS, Git) can cause failures. Install required plugins via <strong>Manage Plugins</strong>.</li><li><strong>Permission Issues</strong>: Ensure Jenkins has access to your GitHub repository. Use a personal access token if needed.</li><li><strong>Node.js Version Mismatch</strong>: Configure the Node.js version in <strong>Manage Jenkins</strong> &gt; <strong>Global Tool Configuration</strong> to match your app’s requirements (e.g., Node 16).</li><li><strong>Slow Builds</strong>: Cache dependencies to speed up builds. Add a caching step using the archiveArtifacts plugin or a shared volume:</li></ol><pre>stage(&#39;Cache Dependencies&#39;) {<br>    steps {<br>        sh &#39;npm install --cache /var/jenkins_home/npm-cache&#39;<br>    }<br>}</pre><h3>What We’ve Learned</h3><p>In this article, we:</p><ul><li>Set up a Jenkins server using Docker.</li><li>Created a Jenkinsfile to define a pipeline for building, testing, and deploying a Node.js app.</li><li>Integrated Jenkins with a GitHub repository.</li><li>Addressed common pitfalls like plugin issues and slow builds.</li></ul><p>This pipeline mirrors the GitHub Actions pipeline from the second article but showcases Jenkins’ flexibility and Pipeline as Code approach.</p><h3>Interconnections: Jenkins in the DevOps Ecosystem</h3><p>This pipeline connects to broader DevOps practices:</p><ul><li><strong>Version Control</strong>: Jenkins integrates with GitHub, triggering builds on code changes.</li><li><strong>Automation</strong>: Automated builds and tests reduce manual effort, aligning with DevOps principles.</li><li><strong>Extensibility</strong>: Jenkins’ plugins enable integration with tools like Docker, Kubernetes, and cloud platforms, which we’ll explore in later articles.</li></ul><h3>What’s Next?</h3><p>In the next article, we’ll dive into <strong>Continuous Integration principles</strong>, focusing on writing effective tests, managing build artifacts, and handling merge conflicts. We’ll also optimize our Jenkins pipeline for speed and reliability. Future articles will cover advanced topics like microservices CI/CD, DevSecOps, and scaling.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a85a4855195f" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Setting Up a Basic CI/CD Pipeline with GitHub Actions]]></title>
            <link>https://medium.com/@imanthasandaumini1/setting-up-a-basic-ci-cd-pipeline-with-github-actions-a02796cb0c4b?source=rss-3fd07f7d72c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/a02796cb0c4b</guid>
            <dc:creator><![CDATA[Sandumini Karunarathne]]></dc:creator>
            <pubDate>Sat, 19 Jul 2025 08:33:51 GMT</pubDate>
            <atom:updated>2025-07-19T10:25:29.890Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/300/1*qSAmnKA3NzY6eF6rzgvLpg.png" /></figure><p>In the first article, we explored what CI/CD pipelines are, why they matter, and their core components. Now, it’s time to get hands-on! In this article, we’ll walk through setting up a <strong>basic CI/CD pipeline</strong> using <strong>GitHub Actions</strong> for a simple web application. Let’s build this pipeline step-by-step, avoiding common pitfalls and making the process as clear as possible.</p><p>By the end of this article, you’ll have a working pipeline that automates building, testing, and deploying a web app. Whether you’re a beginner or looking to solidify your DevOps skills, this guide will help you understand the nuts and bolts of CI/CD in action. Let’s dive in!</p><h3>What We’ll Build</h3><p>We’ll create a CI/CD pipeline for a <strong>simple Node.js web application</strong> using GitHub Actions. The pipeline will:</p><ol><li>Trigger on every code push to the repository.</li><li>Build the application by installing dependencies.</li><li>Run automated tests to ensure the code works.</li><li>Deploy the app to a staging environment (simulated here for learning purposes).</li></ol><p>Don’t worry if you’re not familiar with Node.js — the concepts apply to any programming language, and I’ll explain each step clearly.</p><h3>Prerequisites</h3><p>Before we start, you’ll need:</p><ul><li>A <strong>GitHub account</strong> (free tier is fine).</li><li>A simple Node.js application (we’ll create one below).</li><li>Basic knowledge of Git and JavaScript (helpful but not mandatory).</li></ul><h3>Step 1: Set Up a Sample Node.js Application</h3><p>Let’s create a minimal Node.js web app to work with. If you already have a project, feel free to use it instead.</p><ol><li><strong>Create a new GitHub repository</strong>:</li></ol><ul><li>Go to GitHub, create a new repository (e.g., my-cicd-app), and initialize it with a README.md.</li><li>Clone the repository to your local machine:</li></ul><pre>git clone https://github.com/your-username/my-cicd-app.git<br>cd my-cicd-app</pre><p><strong>2. Set up a basic Node.js app</strong>:</p><ul><li>Initialize a Node.js project:</li></ul><pre>npm init -y</pre><ul><li>Install <strong>Express</strong> (a web framework):</li></ul><pre>npm install express</pre><ul><li>Create a file named app.js with the following code:</li></ul><pre>const express = require(&#39;express&#39;);<br>const app = express();<br><br>app.get(&#39;/&#39;, (req, res) =&gt; {<br>  res.send(&#39;Hello, CI/CD World!&#39;);<br>});<br><br>const port = process.env.PORT || 3000;<br>app.listen(port, () =&gt; {<br>  console.log(`Server running on port ${port}`);<br>});</pre><ul><li>Create a test file named test/app.test.js using <strong>Jest</strong> for testing:</li></ul><pre>npm install --save-dev jest supertest</pre><ul><li>Add this to test/app.test.js:</li></ul><pre>const request = require(&#39;supertest&#39;);<br>const express = require(&#39;express&#39;);<br>const app = express();<br>app.get(&#39;/&#39;, (req, res) =&gt; {<br>  res.send(&#39;Hello, CI/CD World!&#39;);<br>});<br><br>describe(&#39;GET /&#39;, () =&gt; {<br>  it(&#39;should return Hello, CI/CD World!&#39;, async () =&gt; {<br>    const res = await request(app).get(&#39;/&#39;);<br>    expect(res.statusCode).toBe(200);<br>    expect(res.text).toBe(&#39;Hello, CI/CD World!&#39;);<br>  });<br>});</pre><ul><li>Update package.json to include a test script:</li></ul><pre>&quot;scripts&quot;: {<br>  &quot;start&quot;: &quot;node app.js&quot;,<br>  &quot;test&quot;: &quot;jest&quot;<br>}</pre><p><strong>3. Push the code to GitHub</strong>:</p><pre>git add .<br>git commit -m &quot;Initial Node.js app setup&quot;<br>git push origin main</pre><p>Your repository now contains a simple web app with a test suite. Let’s automate it with a CI/CD pipeline!</p><h3>Step 2: Create a GitHub Actions Workflow</h3><p>GitHub Actions is a powerful CI/CD tool integrated into GitHub. It uses YAML files to define workflows (pipelines) that run when specific events occur, like a code push.</p><ol><li><strong>Create a workflow file</strong>:</li></ol><ul><li>In your repository, create a directory .github/workflows.</li><li>Inside it, create a file named ci-cd.yml with the following content:</li></ul><pre>name: CI/CD Pipeline<br><br>on:<br>  push:<br>    branches:<br>      - main<br><br>jobs:<br>  build-and-test:<br>    runs-on: ubuntu-latest<br><br>    steps:<br>      - name: Checkout code<br>        uses: actions/checkout@v3<br><br>      - name: Set up Node.js<br>        uses: actions/setup-node@v3<br>        with:<br>          node-version: &#39;16&#39;<br><br>      - name: Install dependencies<br>        run: npm install<br><br>      - name: Run tests<br>        run: npm test<br><br>      - name: Build<br>        run: npm run build || true</pre><p>Let’s break down this file:</p><ul><li><em>name</em>: The name of the workflow (appears in GitHub’s UI).</li><li><em>on</em>: Specifies when the pipeline runs (on push to the main branch).</li><li><em>jobs</em>: Defines tasks to execute. Here, we have one job (build-and-test).</li><li><em>runs-on</em>: Specifies the environment (Ubuntu latest).</li><li><em>steps</em>: Individual actions, like checking out code, setting up Node.js, installing dependencies, and running tests.</li></ul><p><strong>2. Commit the workflow file</strong>:</p><pre>git add .github/workflows/ci-cd.yml<br>git commit -m &quot;Add GitHub Actions CI/CD pipeline&quot;<br>git push origin main</pre><p><strong>3. Check the pipeline</strong>:</p><ul><li>Go to your GitHub repository, click the <strong>Actions</strong> tab, and you’ll see the pipeline running. If all steps pass, you’ll see green checkmarks!</li></ul><h3>Step 3: Adding Deployment to the Pipeline</h3><p>Now, let’s extend the pipeline to deploy the app to a <strong>staging environment</strong>. For simplicity, we’ll simulate deployment by printing a message, but in a real project, you’d deploy to a platform like Heroku, Vercel, or AWS.</p><p>Update the ci-cd.yml file to include a deployment step:</p><pre>name: CI/CD Pipeline<br><br>on:<br>  push:<br>    branches:<br>      - main<br><br>jobs:<br>  build-and-test:<br>    runs-on: ubuntu-latest<br><br>    steps:<br>      - name: Checkout code<br>        uses: actions/checkout@v3<br><br>      - name: Set up Node.js<br>        uses: actions/setup-node@v3<br>        with:<br>          node-version: &#39;16&#39;<br><br>      - name: Install dependencies<br>        run: npm install<br><br>      - name: Run tests<br>        run: npm test<br><br>      - name: Build<br>        run: npm run build || true<br><br>      - name: Deploy to staging<br>        run: echo &quot;Deploying to staging environment...&quot;<br>        # In a real project, add deployment commands here (e.g., to Heroku)</pre><p>Commit and push this change:</p><pre>git add .github/workflows/ci-cd.yml<br>git commit -m &quot;Add deployment step to CI/CD pipeline&quot;<br>git push origin main</pre><h3>Common Pitfalls and How to Avoid Them</h3><p>As a beginner, I’ve run into a few hiccups while setting up pipelines. Here are some common issues and solutions:</p><ol><li><strong>Flaky Tests</strong>: Tests might fail intermittently due to timing issues or external dependencies. Ensure tests are isolated and deterministic. For our app, supertest makes API testing reliable.</li><li><strong>Missing Dependencies</strong>: If npm install fails, check that all dependencies are listed in package.json. Use npm ci in CI for consistent installs.</li><li><strong>YAML Syntax Errors</strong>: A misplaced space in ci-cd.yml can break the pipeline. Use GitHub’s editor or a YAML linter to validate your file.</li><li><strong>Long Build Times</strong>: Cache dependencies to speed up builds. Add this step before npm install:</li></ol><pre>- name: Cache dependencies<br>  uses: actions/cache@v3<br>  with:<br>    path: ~/.npm<br>    key: ${{ runner.os }}-node-${{ hashFiles(&#39;**/package-lock.json&#39;) }}</pre><h3>What We’ve Learned</h3><p>In this article, we:</p><ul><li>Set up a simple Node.js app with tests.</li><li>Created a GitHub Actions pipeline to automate building and testing.</li><li>Added a simulated deployment step.</li><li>Identified common pitfalls and their solutions.</li></ul><p>This pipeline is basic but functional, covering the core CI/CD stages: <strong>source control</strong>, <strong>build</strong>, <strong>test</strong>, and <strong>deploy</strong>. In real projects, you’d extend this with more complex tests, multiple environments, and production deployments.</p><h3>Interconnections: CI/CD in Context</h3><p>This pipeline connects to broader DevOps practices:</p><ul><li><strong>Version Control</strong>: GitHub is our source control, and the pipeline triggers on commits, ensuring every change is tested.</li><li><strong>Automation</strong>: Automated builds and tests reduce manual effort, aligning with DevOps’ focus on efficiency.</li><li><strong>Team Collaboration</strong>: By catching issues early, the pipeline fosters trust among developers, testers, and operations teams.</li></ul><h3>What’s Next?</h3><p>We’ll walk through setting up a basic CI/CD pipeline using <strong>Jenkins </strong>for a simple web application. You’ll learn how to write a pipeline configuration, automate builds, and run tests — all while avoiding common pitfalls.</p><h3>Try It Yourself!</h3><p>Clone the repository we built, tweak the pipeline, and experiment with adding new steps (e.g., linting or deploying to Heroku).</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a02796cb0c4b" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introduction to CI/CD Pipelines]]></title>
            <link>https://medium.com/@imanthasandaumini1/introduction-to-ci-cd-pipelines-92b7c55f2a9e?source=rss-3fd07f7d72c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/92b7c55f2a9e</guid>
            <dc:creator><![CDATA[Sandumini Karunarathne]]></dc:creator>
            <pubDate>Sat, 19 Jul 2025 07:54:54 GMT</pubDate>
            <atom:updated>2025-07-19T07:54:54.264Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/834/1*Qmie5lY2cGPKxgg_0Du0_w.png" /></figure><h3>Backbone of Modern Software Delivery</h3><p>Whether you’re a beginner looking to understand CI/CD or a professional aiming to refine your DevOps skills, this series will guide you through every aspect of building, optimizing, and scaling CI/CD pipelines. Let’s start with the basics: What are CI/CD pipelines, why do they matter, and how do they work?</p><h3>What Are CI/CD Pipelines?</h3><p>A <strong>CI/CD pipeline</strong> is an automated workflow that helps software teams deliver code changes faster, more reliably, and with fewer errors. It’s a cornerstone of modern software development, enabling teams to move from writing code to deploying it in production seamlessly. Let’s break it down:</p><ul><li><strong>Continuous Integration (CI)</strong>: CI focuses on automating and improving the process of integrating code changes from multiple developers into a shared repository. Developers commit code frequently (often multiple times a day), and each commit triggers an automated build and test process to catch issues early.</li><li><strong>Continuous Delivery (CD)</strong>: CD extends CI by automating the deployment of code to a staging or production-like environment. Every change that passes the CI process is ready to be deployed, but deployment to production requires manual approval.</li><li><strong>Continuous Deployment</strong>: A more advanced form of CD, where every change that passes the pipeline is automatically deployed to production without manual intervention.</li></ul><p>Think of a CI/CD pipeline as an assembly line for software: raw code goes in, and a polished, tested, and deployable product comes out.</p><h3>Why CI/CD Matters</h3><p>As software development becomes faster-paced, manual processes like testing and deployment can’t keep up. CI/CD pipelines address this by:</p><ul><li><strong>Speeding up delivery</strong>: Automated pipelines reduce the time from code commit to production deployment.</li><li><strong>Improving quality</strong>: Automated tests catch bugs early, ensuring higher-quality software.</li><li><strong>Reducing risks</strong>: Frequent, small changes are easier to test and deploy than large, infrequent updates.</li><li><strong>Enhancing collaboration</strong>: CI/CD encourages developers, testers, and operations teams to work together seamlessly.</li></ul><p>For businesses, CI/CD translates to faster feature releases, happier customers, and a competitive edge. For developers, it means less time on repetitive tasks and more focus on writing great code.</p><h3>Key Components of a CI/CD Pipeline</h3><p>A typical CI/CD pipeline consists of several stages, each automated to ensure smooth progression from code to deployment:</p><ol><li><strong>Source Control</strong>: Code is stored in a version control system (e.g., Git, hosted on GitHub, GitLab, or Bitbucket). Developers push changes to a shared repository.</li><li><strong>Build</strong>: The code is compiled or packaged into an executable or deployable artifact (e.g., a Docker image or a JAR file).</li><li><strong>Test</strong>: Automated tests (unit, integration, or end-to-end) verify the code’s functionality and quality.</li><li><strong>Deploy</strong>: The tested code is deployed to a staging or production environment.</li></ol><p>Here’s a simple example of how this might look for a web application:</p><ol><li>A developer pushes a code change to a GitHub repository.</li><li>A CI/CD tool (e.g., GitHub Actions) detects the change and triggers a pipeline.</li><li>The pipeline builds the application (e.g., compiles a Node.js app).</li><li>It runs unit tests to ensure the code works as expected.</li><li>If tests pass, the code is deployed to a staging server for further testing or directly to production.</li></ol><h3>A Brief History of CI/CD</h3><p>The roots of CI/CD lie in the <strong>agile movement</strong> and <strong>extreme programming (XP)</strong> practices from the early 2000s. Continuous Integration emerged as a way to address the “integration hell” developers faced when merging large codebases. Tools like <strong>CruiseControl</strong> (2001) and <strong>Jenkins</strong> (2011) popularized CI by automating builds and tests.</p><p>Continuous Delivery and Deployment evolved as teams sought to automate the entire release process. Today, tools like <strong>GitLab CI</strong>, <strong>GitHub Actions</strong>, and <strong>CircleCI</strong> have made CI/CD accessible to teams of all sizes, while cloud platforms like AWS and Azure have integrated CI/CD into their ecosystems.</p><h3>Common CI/CD Tools</h3><p>There are many tools to build CI/CD pipelines, each with unique strengths:</p><ul><li><strong>Jenkins</strong>: An open-source, highly customizable CI/CD server.</li><li><strong>GitHub Actions</strong>: A cloud-based CI/CD tool integrated with GitHub repositories.</li><li><strong>GitLab CI</strong>: A robust CI/CD system built into GitLab, ideal for end-to-end DevOps.</li><li><strong>CircleCI</strong>: A cloud-native CI/CD tool known for speed and simplicity.</li><li><strong>Azure DevOps</strong>: A Microsoft solution for CI/CD and DevOps workflows.</li></ul><p>In future articles, we’ll dive deeper into these tools, including hands-on examples of setting up pipelines with them.</p><h3>Challenges in</h3><p>While CI/CD pipelines are powerful, they’re not without challenges. As a beginner, I’ve learned that setting up pipelines involves navigating issues like:</p><ul><li><strong>Flaky tests</strong>: Tests that pass or fail inconsistently, undermining trust in the pipeline.</li><li><strong>Complex configurations</strong>: Writing pipeline scripts can be daunting for newcomers.</li><li><strong>Environment mismatches</strong>: Differences between development, staging, and production environments can cause deployment failures.</li><li><strong>Security risks</strong>: Misconfigured pipelines can expose sensitive data or vulnerabilities.</li></ul><p>This series will tackle these challenges head-on, providing practical solutions and best practices to help you build robust pipelines.</p><h3>What’s Next in This Series?</h3><p>This is just the beginning! In the next article, we’ll walk through setting up a basic CI/CD pipeline using <strong>GitHub Actions</strong> for a simple web application. You’ll learn how to write a pipeline configuration, automate builds, and run tests — all while avoiding common pitfalls. Later articles will explore advanced topics like microservices CI/CD, DevSecOps, and scaling pipelines for large teams.</p><p>By the end of this series, you’ll not only understand CI/CD pipelines but also be equipped to design and implement them in real-world projects. Whether you’re aiming to boost your career as a DevOps engineer or simply want to streamline your team’s workflow, this series will be your guide.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=92b7c55f2a9e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Meta’s Next-Generation Communication App “Threads”]]></title>
            <link>https://medium.com/@imanthasandaumini1/metas-next-generation-communication-app-threads-68d7dca58b31?source=rss-3fd07f7d72c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/68d7dca58b31</guid>
            <dc:creator><![CDATA[Sandumini Karunarathne]]></dc:creator>
            <pubDate>Tue, 18 Jul 2023 06:08:14 GMT</pubDate>
            <atom:updated>2023-07-18T06:12:41.068Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*oLSeTxIZqkaQWwjE.jpg" /></figure><p>In the era of social media and instant messaging, Meta (formerly Facebook) has introduced a new app called Threads. Designed as a companion to Instagram, Threads aims to provide users with a more intimate and private way to connect with their close friends.</p><p>News and politics are minimized on Meta’s Threads, a Twitter clone. By minimizing news and politics, Meta’s new app, Threads, is believed to be a gentler alternative to Twitter.</p><p><strong>How is the Threads?</strong><br>Threads is designed for quick text chats, just like Twitter and other comparable social media platforms like Mastodon and Bluesky. These quick posts are referred to as threads rather than Tweets, Toots (Mastodon), or Skeets (Bluesky).</p><p><strong>How is Twitter different from threads?</strong><br>Twitter’s free users are limited to 240 characters for each post, compared to 500 for Thread users. In a similar vein, videos on Twitter are limited to 512MB and up to 2 minutes, 20 seconds, while the maximum on Threads is increased to 5 minutes.</p><p><strong>What is the Instagram thread app?</strong><br>Launched on July 5, 2023, Instagram’s Threads is a text-based chat app.</p><p><strong>Does Threads require an Instagram account?</strong><br>To sign up for Threads, you must have an Instagram account; after that, the two profiles will be connected. This implies that a person’s Instagram handle will also serve as their Threads username and that the name or nickname they enter on Instagram will appear as their name on Threads.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/634/0*rpANHbCzm9yJ0Z6q.jpg" /></figure><p><strong><em>Let’s delve into more details about the features and functionalities of Threads:</em></strong></p><p><strong>Status Updates:</strong> Threads allows users to easily share their status with their close friends. It goes beyond the generic status updates by offering automatic status suggestions based on your location, movement, and even the battery level of your device. This feature enables friends to stay connected and informed about each other’s activities effortlessly.</p><p><strong>Close Friends List:</strong> In Threads, you can create a separate list of your close friends on Instagram. This list helps you prioritize the content you see on the app, ensuring that you don’t miss updates from your inner circle amidst the noise of the broader Instagram feed.</p><p><strong>Direct Messaging:</strong> The app places a strong emphasis on direct messaging, making it convenient to have one-on-one conversations or engage in group chats with your close friends. The messaging experience in Threads is simple and streamlined, allowing for quick text-based conversations.</p><p><strong>Camera Integration:</strong> Threads integrates seamlessly with your smartphone’s camera, making it effortless to capture and share photos and videos with your close friends. The camera opens directly in the app, enabling you to quickly snap and send visual updates to your chosen recipients.</p><p><strong>Privacy and Control:</strong> Meta has prioritized privacy in Threads. The app provides granular control over who can reach you and view your updates. You can customize your preferences to ensure that only your approved close friends can see your content. This feature fosters a sense of security and trust among users.</p><p><strong>Notifications and Reminders:</strong> Threads offers customizable notifications, allowing you to stay updated without feeling overwhelmed. You can choose to receive notifications for specific friends or even opt for a silent mode to enjoy uninterrupted focus when needed. Additionally, you can set reminders to stay in touch with your close friends regularly.</p><p><strong>Minimal News and Politics:</strong> Unlike many other social media platforms, Threads minimizes the presence of news and politics in the app. It aims to create a more relaxed and intimate space for users to connect and share personal moments with their close friends.</p><h3><strong>Will it be another time-wasting app for the younger generation?????👩‍💻</strong></h3><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=68d7dca58b31" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>