Skip to content

Component Testing in Svelte

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Component Testing in Svelte

Testing helps us trust our application, and it's a safety net for future changes. In this tutorial, we will set up our Svelte project to run tests for our components.

Starting a new project

Let's start by creating a new project:

pnpm dlx create-vite
// Project name: ā€ŗ testing-svelte
// Select a framework: ā€ŗ svelte
// Select a variant: ā€ŗ svelte-ts

cd testing-svelte
pnpm install

There are other ways of creating a Svelte project, but I prefer using Vite. One of the reasons that I prefer using Vite is that SvelteKit will use it as well. I'm also a big fan of pnpm, but you can use your preferred package manager. Make sure you follow Vite's docs on starting a new project using npm or yarn.

Installing required dependencies

  • Jest: I'll be using this framework for testing. It's the one that I know best, and feel more comfortable with. Because I'm using TypeScript, I need to install its type definitions too.
  • ts-jest: A transformer for handling TypeScript files.
  • svelte-jester: precompiles Svelte components before tests.
  • Testing Library: Doesn't matter what framework I'm using, I will look for an implementation of this popular library.
pnpm install --save-dev jest @types/jest @testing-library/svelte svelte-jester ts-jest

Configuring tests

Now that our dependencies are installed, we need to configure jest to prepare the tests and run them.

A few steps are required:

  • Convert *.ts files
  • Complile *.svelte files
  • Run the tests

Create a configuration file at the root of the project:

// jest.config.js
export default {
  transform: {
    '^.+\\.ts$': 'ts-jest',
    '^.+\\.svelte$': [
      'svelte-jester',
      {
        preprocess: true,
      },
    ],
  },
  moduleFileExtensions: ['js', 'ts', 'svelte'],
};

Jest will now use ts-jest for compiling *.ts files, and svelte-jester for *.svelte files.

Creating a new test

Let's test the Counter component created when we started the project, but first, I'll check what our component does.

<script lang="ts">
  let count: number = 0;
  const increment = () => {
    count += 1;
  };
</script>

<button on:click={increment}>
  Clicks: {count}
</button>

<style>
  button {
    font-family: inherit;
    font-size: inherit;
    padding: 1em 2em;
    color: #ff3e00;
    background-color: rgba(255, 62, 0, 0.1);
    border-radius: 2em;
    border: 2px solid rgba(255, 62, 0, 0);
    outline: none;
    width: 200px;
    font-variant-numeric: tabular-nums;
    cursor: pointer;
  }

  button:focus {
    border: 2px solid #ff3e00;
  }

  button:active {
    background-color: rgba(255, 62, 0, 0.2);
  }
</style>

This is a very small component where a button when clicked, updates a count, and that count is reflected in the button text. So, that's exactly what we'll be testing.

I'll create a new file ./lib/__tests__/Counter.spec.ts

/**
 * @jest-environment jsdom
 */

import { render, fireEvent } from '@testing-library/svelte';
import Counter from '../Counter.svelte';

describe('Counter', () => {
  it('it changes count when button is clicked', async () => {
    const { getByText } = render(Counter);
    const button = getByText(/Clicks:/);
    expect(button.innerHTML).toBe('Clicks: 0');
    await fireEvent.click(button);
    expect(button.innerHTML).toBe('Clicks: 1');
  });
});

We are using render and fireEvent from testing-library. Be mindful that fireEvent returns a Promise and we need to await for it to be fulfilled. I'm using the getByText query, to get the button being clicked. The comment at the top, informs jest that we need to use jsdom as the environment. This will make things like document available, otherwise, render will not be able to mount the component. This can be set up globally in the configuration file.

What if we wanted, to test the increment method in our component? If it's not an exported function, I'd suggest testing it through the rendered component itself. Otherwise, the best option is to extract that function to another file, and import it into the component.

Let's see how that works.

// lib/increment.ts
export function increment (val: number) {
    val += 1;
    return val
  };
<!-- lib/Counter.svelte -->
<script lang="ts">
  import { increment } from './increment';
  let count: number = 0;
</script>

<button on:click={() => (count = increment(count))}>
  Clicks: {count}
</button>
<!-- ... -->

Our previous tests will still work, and we can add a test for our function.

// lib/__tests__/increment.spec.ts

import { increment } from '../increment';

describe('increment', () => {
  it('it returns value+1 to given value when called', async () => {
    expect(increment(0)).toBe(1);
    expect(increment(-1)).toBe(0);
    expect(increment(1.2)).toBe(2.2);
  });
});

In this test, there's no need to use jsdom as the test environment. We are just testing the function.

If our method was exported, we can then test it by accessing it directly.

<!-- lib/Counter.svelte -->
<script lang="ts">
  let count: number = 0;
  export const increment = () => {
    count += 1;
  };
</script>

<button on:click={increment}>
  Clicks: {count}
</button>
<!-- ... -->
// lib/__tests__/Counter.spec.ts

describe('Counter Component', () => {
 // ... other tests

  describe('increment', () => {
    it('it exports a method', async () => {
      const { component } = render(Counter);
      expect(component.increment).toBeDefined();
    });

    it('it exports a method', async () => {
      const { getByText, component } = render(Counter);
      const button = getByText(/Clicks:/);
      expect(button.innerHTML).toBe('Clicks: 0');
      await component.increment()
      expect(button.innerHTML).toBe('Clicks: 1');
    });
  });
});

When the method is exported, you can access it directly from the returned component property of the render function.

NOTE: I don't recommend exporting methods from the component for simplicity if they were not meant to be exported. This will make them available from the outside, and callable from other components.

Events

If your component dispatches an event, you can test it using the component property returned by render.

To dispatch an event, we need to import and call createEventDispatcher, and then call the returning funtion, giving it an event name and an optional value.

<!-- lib/Counter.svelte -->
<script lang="ts">
  import { createEventDispatcher } from 'svelte';
  const dispatch = createEventDispatcher();

  let count: number = 0;
  export const increment = () => {
    count += 1;
    dispatch('countChanged', count);
  };
</script>

<button on:click={increment}>
  Clicks: {count}
</button>
<!-- ... -->
// lib/__tests__/Counter.spec.ts
// ...

  it('it emits an event', async () => {
    const { getByText, component } = render(Counter);
    const button = getByText(/Clicks:/);
    let mockEvent = jest.fn();
    component.$on('countChanged', function (event) {
      mockEvent(event.detail);
    });
    await fireEvent.click(button);

    // Some examples on what to test
    expect(mockEvent).toHaveBeenCalled(); // to check if it's been called
    expect(mockEvent).toHaveBeenCalledTimes(1); // to check how any times it's been called
    expect(mockEvent).toHaveBeenLastCalledWith(1); // to check the content of the event
    await fireEvent.click(button);
    expect(mockEvent).toHaveBeenCalledTimes(2);
    expect(mockEvent).toHaveBeenLastCalledWith(2);
  });

//...

For this example, I updated the component to emit an event: countChanged. Every time the button is clicked, the event will emit the new count. In the test, I'm using getByText to select the button to click, and component.

Then, I'm using component.$on(eventName), and mocking the callback function to test the emitted value (event.detail).

Props

You can set initial props values, and modifying them using the client-side component API.

Let's update our component to receive the initial count value.

<!-- lib/Counter.svelte -->
<script lang="ts">
// ...
  export let count: number = 0;
// ...
</script>

<!-- ... -->

Converting count to an input value requires exporting the variable declaration.

Then we can test:

  • default values
  • initial values
  • updating values
// lib/__tests__/Counter.ts
// ...
describe('count', () => {
    it('defaults to 0', async () => {
      const { getByText } = render(Counter);
      const button = getByText(/Clicks:/);
      expect(button.innerHTML).toBe('Clicks: 0');
    });

    it('can have an initial value', async () => {
      const { getByText } = render(Counter, {props: {count: 33}});
      const button = getByText(/Clicks:/);
      expect(button.innerHTML).toBe('Clicks: 33');
    });

    it('can be updated', async () => {
      const { getByText, component } = render(Counter);
      const button = getByText(/Clicks:/);
      expect(button.innerHTML).toBe('Clicks: 0');
      await component.$set({count: 41})
      expect(button.innerHTML).toBe('Clicks: 41');
    });
});
// ...

We are using the second argument of the render method to pass initial values to count, and we are testing it through the rendered button

To update the value, we are calling the $set method on component, which will update the rendered value on the next tick. That's why we need to await it.

Wrapping up

Testing components using Jest and Testing Library can help you avoid errors when developing, and also can make you more confident when applying changes to an existing codebase. I hope this blog post is a step forward to better testing.

You can find these examples in this repo

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

D1 SQLite: Writing queries with the D1 Client API cover image

D1 SQLite: Writing queries with the D1 Client API

Writing queries with the D1 Client API In the previous post we defined our database schema, got up and running with migrations, and loaded some seed data into our database. In this post we will be working with our new database and seed data. If you want to participate, make sure to follow the steps in the first post. Weā€™ve been taking a minimal approach so far by using only wrangler and sql scripts for our workflow. The D1 Client API has a small surface area. Thanks to the power of SQL, we will have everything we need to construct all types of queries. Before we start writing our queries, let's touch on some important concepts. Prepared statements and parameter binding This is the first section of the docs and it highlights two different ways to write our SQL statements using the client API: prepared and static statements. Best practice is to use prepared statements because they are more performant and prevent SQL injection attacks. So we will write our queries using prepared statements. We need to use parameter binding to build our queries with prepared statements. This is pretty straightforward and there are two variations. By default we add ? ā€™s to our statement to represent a value to be filled in. The bind method will bind the parameters to each question mark by their index. The first ? is tied to the first parameter in bind, 2nd, etc. I would stick with this most of the time to avoid any confusion. ` I like this second method less as it feels like something I can imagine messing up very innocently. You can add a number directly after a question mark to indicate which number parameter it should be bound to. In this exampl, we reverse the previous binding. ` Reusing prepared statements If we take the first example above and not bind any values we have a statement that can be reused: ` Querying For the purposes of this post we will just build example queries by writing them out directly in our Worker fetch handler. If you are building an app I would recommend building functions or some other abstraction around your queries. select queries Let's write our first query against our data set to get our feet wet. Hereā€™s the initial worker code and a query for all authors: ` We pass our SQL statement into prepare and use the all method to get all the rows. Notice that we are able to pass our types to a generic parameter in all. This allows us to get a fully typed response from our query. We can run our worker with npm run dev and access it at http://localhost:8787 by default. Weā€™ll keep this simple workflow of writing queries and passing them as a json response for inspection in the browser. Opening the page we get our author results. joins Not using an ORM means we have full control over our own destiny. Like anything else though, this has tradeoffs. Letā€™s look at a query to fetch the list of posts that includes author and tags information. ` Letā€™s walk through each part of the query and highlight some pros and cons. ` * The query selects all columns from the posts table. * It also selects the name column from the authors table and renames it to author_name. * It aggregates the name column from the tags table into a JSON array. If there are no tags, it returns an empty JSON array. This aggregated result is renamed to tags. ` * The query starts by selecting data from the posts table. * It then joins the authors table to include author information for each post, matching posts to authors using the author_id column in posts and the id column in authors. * Next, it left joins the posts_tags table to include tag associations for each post, ensuring that all posts are included even if they have no tags. * Next, it left joins the tags table to include tag names, matching tags to posts using the tag_id column in posts_tags and the id column in tags. * Finally, group the results by the post id so that all rows with the same post id are combined in a single row SQL provides a lot of power to query our data in interesting ways. JOIN ā€™s will typically be more performant than performing additional queries.You could just as easily write a simpler version of this query that uses subqueries to fetch post tags and join all the data by hand with JavaScript. This is the nice thing about writing SQL, youā€™re free to fetch and handle your data how you please. Our results should look similar to this: ` This brings us to our next topic. Marshaling / coercing result data A couple of things we notice about the format of the result data our query provides: Rows are flat. We join the author directly onto the post and prefix its column names with author. ` Using an ORM we might get the data back as a child object: ` Another thing is that our tags data is a JSON string and not a JavaScript array. This means that we will need to parse it ourselves. ` This isnā€™t the end of the world but it is some more work on our end to coerce the result data into the format that we actually want. This problem is handled in most ORMā€™s and is their main selling point in my opinion. insert / update / delete Next, letā€™s write a function that will add a new post to our database. ` Thereā€™s a few queries involved in our create post function: * first we create the new post * next we run through the tags and either create or return an existing tag * finally, we add entries to our post_tags join table to associate our new post with the tags assigned We can test our new function by providing post content in query params on our index page and formatting them for our function. ` I gave it a run like this: http://localhost:8787authorId=1&tags=Food%2CReview&title=A+review+of+my+favorite+Italian+restaurant&content=I+got+the+sausage+orchette+and+it+was+amazing.+I+wish+that+instead+of+baby+broccoli+they+used+rapini.+Otherwise+it+was+a+perfect+dish+and+the+vibes+were+great And got a new post with the id 11. UPDATE and DELETE operations are pretty similar to what weā€™ve seen so far. Most complexity in your queries will be similar to what weā€™ve seen in the posts query where we want to JOIN or GROUP BY data in various ways. To update the post we can write a query that looks like this: ` COALESCE acts similarly to if we had written a ?? b in JavaScript. If the binded value that we provide is null it will fall back to the default. We can delete our new post with a simple DELETE query: ` Transactions / Batching One thing to note with D1 is that I donā€™t think the traditional style of SQLite transactions are supported. You can use the db.batch API to achieve similar functionality though. According to the docs: Batched statements are SQL transactions ā†—. If a statement in the sequence fails, then an error is returned for that specific statement, and it aborts or rolls back the entire sequence. ` Summary In this post, we've taken a hands-on approach to exploring the D1 Client API, starting with defining our database schema and loading seed data. We then dove into writing queries, covering the basics of prepared statements and parameter binding, before moving on to more complex topics like joins and transactions. We saw how to construct and execute queries to fetch data from our database, including how to handle relationships between tables and marshal result data into a usable format. We also touched on inserting, updating, and deleting data, and how to use transactions to ensure data consistency. By working through these examples, we've gained a solid understanding of how to use the D1 Client API to interact with our database and build robust, data-driven applications....

Incremental Hydration in Angular cover image

Incremental Hydration in Angular

Incremental Hydration in Angular Some time ago, I wrote a post about SSR finally becoming a first-class citizen in Angular. It turns out that the Angular team really treats SSR as a priority, and they have been working tirelessly to make SSR even better. As the previous blog post mentioned, full-page hydration was launched in Angular 16 and made stable in Angular 17, providing a great way to improve your Core Web Vitals. Another feature aimed to help you improve your INP and other Core Web Vitals was introduced in Angular 17: deferrable views. Using the @defer blocks allows you to reduce the initial bundle size and defer the loading of heavy components based on certain triggers, such as the section entering the viewport. Then, in September 2024, the smart folks at Angular figured out that they could build upon those two features, allowing you to mark parts of your application to be server-rendered dehydrated and then hydrate them incrementally when needed - hence incremental hydration. Iā€™m sure you know what hydration is. In short, the server sends fully formed HTML to the client, ensuring that the user sees meaningful content as quickly as possible and once JavaScript is loaded on the client side, the framework will reconcile the rendered DOM with component logic, event handlers, and state - effectively hydrating the server-rendered content. But what exactly does "dehydrated" mean, you might ask? Here's what will happen when you mark a part of your application to be incrementally hydrated: 1. Server-Side Rendering (SSR): The content marked for incremental hydration is rendered on the server. 2. Skipped During Client-Side Bootstrapping: The dehydrated content is not initially hydrated or bootstrapped on the client, reducing initial load time. 3. Dehydrated State: The code for the dehydrated components is excluded from the initial client-side bundle, optimizing performance. 4. Hydration Triggers: The application listens for specified hydration conditions (e.g., on interaction, on viewport), defined with a hydrate trigger in the @defer block. 5. On-Demand Hydration: Once the hydration conditions are met, Angular downloads the necessary code and hydrates the components, allowing them to become interactive without layout shifts. How to Use Incremental Hydration Thanks to Mark Thompson, who recently hosted a feature showcase on incremental hydration, we can show some code. The first step is to enable incremental hydration in your Angular application's appConfig using the provideClientHydration provider function: ` Then, you can mark the components you want to be incrementally hydrated using the @defer block with a hydrate trigger: ` And that's it! You now have a component that will be server-rendered dehydrated and hydrated incrementally when it becomes visible to the user. But what if you want to hydrate the component on interaction or some other trigger? Or maybe you don't want to hydrate the component at all? The same triggers already supported in @defer blocks are available for hydration: - idle: Hydrate once the browser reaches an idle state. - viewport: Hydrate once the component enters the viewport. - interaction: Hydrate once the user interacts with the component through click or keydown triggers. - hover: Hydrate once the user hovers over the component. - immediate: Hydrate immediately when the component is rendered. - timer: Hydrate after a specified time delay. - when: Hydrate when a provided conditional expression is met. And on top of that, there's a new trigger available for hydration: - never: When used, the component will remain static and not hydrated. The never trigger is handy when you want to exclude a component from hydration altogether, making it a completely static part of the page. Personally, I'm very excited about this feature and can't wait to try it out. How about you?...

What is Cypress Studio? cover image

What is Cypress Studio?

Introduction Cypress Studio has been around for some time. It was introduced in Cypress v6.3.0, removed in v10, and reintroduced in v10.7.0. This blog post will dive into its current state and how to use it today. What is Cypress Studio Cypress Studio is a tool built on top of Cypress. It offers an interface for creating tests as if you were using your site without requiring code to be written. It adds functionality to query elements and to add assertions from the interface. It's important to note that Cypress Studio is still under an experimental flag and lacks support for Component Testing (another feature of Cypress). Creating an app to test We first need a working app because the tool we will use is meant for end-to-end tests. I'll pick Svelte and Vite for this demo, but you can choose any framework. Cypress is agnostic in that matter. ` Make sure to select the Svelte and Typescript options to follow along. Now open your project location, install dependencies, and start your app to ensure everything works correctly. ` Installing and setting up Cypress Studio Now it's time to add Cypress to our dev dependencies. ` With our dependency installed, let's launch it so we can start configuring it. But first letā€™s add an entry under scripts in our package.json file. ` ` A new window will open that will guide us through the initial setup. Select the "E2E Testing" option to create the required configuration files. Then, you can close the window. A new folder called cypress and a configuration file cypress.config.ts are created at the root of our project. To enable Cypress Studio, we must open the newly created configuration file and add the experimentalStudio property. ` For this project, we need to ensure that TypeScript is configured properly for Cypress when it runs, as it conflicts with the one in our root folder. We will extend the original typescript configuration and override some properties. Create a new tsconfig.json file inside the' cypress' folder. ` You can find more information on setting up TypeScript with Cypress in their documentation Creating tests Now that our setup is complete let's write our first test. We will create a folder called e2e and a new test file. ` Open the new file and create an outline of your tests. Don't add anything to them. I initialized my file with a few tests. ` There is an option to create spec files from the interface, but it will contain some initial content that you most likely remove. Thereā€™s third option to scaffold multiple specs files, these can serve as a learning resource for writing tests as it contains many different scenarios tested. Make sure your app is running and, in another terminal, start cypress. ` Select E2E testing and Chrome. You should now see a list with all your test files. Click on home.cy.ts. Your tests will run and pass because we have not made any assertions. This looks the same as if we haven't enabled Studio, but there's a new detail added when you hover on any of the tests: a magic wand that you can click to start recording events and adding assertions directly in the UI. Let's start with our first tests and check what assertions we can make. The first step is to navigate to a page. This interaction will be recorded and added to the test. To make assertions, right-click on an element and select the ones you want to add. After we complete our test, let's review our generated code. Go to the test file and see the changes made to it. It should look something like this. ` Cypress Studio will attempt to pick the best selector for the element. In this case, it was able to choose button because it is unique to the page. If there were more buttons, the selector would've been different. You can interact with elements as if you were using the page regularly. Note that Studio supports a limited set of commands: check, click, select, type, and uncheck. Let's record our second test and verify that the button will increase its count when clicked. That was quick. Let's review our generated test. ` So far, our tests have been very accurate and didn't need any modifications. However, take Studio as a helper to write tests visually and their selector as suggestions. Let's take the bottom link with the text "SvelteKit" as an example. It will generate the assertion: ` This selector is accurate, but modifying the elements' structure or order would break the tests. We donā€™t want tests to break for reasons that do not affect our users. A change in order does not have the same impact for the user as changing the text of a button or changing the url of a link. These selectors should be as specific as possible, but rely as less as possible on implementation details. Selecting images by its alt attribute itā€™s a way to find a specific element and at the same time ensure that element is accessible. (Accessibility has a direct impact on users) As a counterpart, when clicking any of the logos above, we will get very interesting selectors (and assertions). ` With these assertions, we check that our images have an alt attribute set and are tied to a specified link. Conclusion As with any other automated tool, check the output and solve any issues that you may find. Results depend on the structure of the page and Cypress Studio's ability to choose an adequate selector. Overall, this tool can still help you write complex tests with plenty of interactions, and you can later modify any selectors. Besides the selector, I found it helpful in writing the assertions. If youā€™re new to Cypress or e2e testing, it can be of great help to go from an idea to a working test. Reviewing these tests can be a great way to learn how these tests are built....

ā€œIt Sounds a Little Dystopian, But Also Kind of Amazingā€: Conversations on Long Term AI Agents and "Winning" Product Hunt with Ellie Zubrowski cover image

ā€œIt Sounds a Little Dystopian, But Also Kind of Amazingā€: Conversations on Long Term AI Agents and "Winning" Product Hunt with Ellie Zubrowski

Ellie Zubrowski doesnā€™t walk a traditional path. In the three years since graduating from a university program in Business Administration, she biked across the U.S., studied Kung Fu in China, learned Mandarin just for fun, and completed the #100DaysOfCode challenge after deciding she wanted a career switch. That same sense of curiosity and willingness to jump into the unknown now fuels her work as a Developer Advocate at Pieces, where she leads product launches, mentors job seekers, and helps developers learn how to best leverage Piecesā€™ Long-Term Memory Agent. Her journey into tech was guided not just by a want to learn how to code and break into the industry, but by a fascination with the structure of language itself. > ā€œThere are so many parallels between human languages and programming languages,ā€ she says. ā€œThat realization really made me fall in love with software.ā€ > We spoke with Ellie about launching a #1 Product Hunt release, her predictions for AI agents, and why conferences donā€™t have to break your budget. Launching LTM-2 to the Top of Product Hunt Recently, Ellie led the launch of Piecesā€™ Long-Term Memory Agent (LTM-2), which took the top spot on Product Huntā€”a major win for the team and their community. > ā€œIā€™m super competitive,ā€ she admits. ā€œSo I really wanted us to win.ā€ The launch was fully organicā€”no paid promotions, just coordinated team efforts, a well-prepared content pipeline, and an ambassador program that brought in authentic engagement across X, Discord, and Reddit. She documented their entire strategy in this blog post, and credits the success not just to good planning but to a passionate developer community that believed in the product. Following a successful performance at Product Hunt, Ellie is committed to keeping Piecesā€™ user community engaged and contributing to its technological ecosystem. > ā€œAlthough Iā€™m still fairly new to DevRel (coming up on a year at Pieces!), I think success comes down to a few things: developer adoption and retention, user feedback, community engagement, and maintaining communication with engineering.ā€ Why AI Agents Are the Next Big Thing Ellie sees a major shift on the horizon: AI that doesnā€™t wait for a prompt. > ā€œThe biggest trend of 2025 seems to be AI agents,ā€ she explains, ā€œor AI that acts proactively instead of reactively.ā€ Until now, most of us have had to tell AI exactly what to doā€”whether thatā€™s drafting emails, debugging code, or generating images. But Ellie imagines a near future where AI tools act more like intelligent teammates than assistantsā€”running locally, deeply personalized, and working in the background to handle the repetitive stuff. > ā€œImagine something that knows how you work and quietly handles your busy work while you focus on the creative parts,ā€ she says. ā€œIt sounds a little dystopian, but also kind of amazing.ā€ Whether we hit that level of autonomy in 2025 or (likely) have to wait until 2026, she believes the move toward agentic AI is inevitableā€”and itā€™s changing how developers think about productivity, ownership, and trust. You can read more of Ellieā€™s 2025 LLM predictions here! The Secret to Free Conferences (and Winning the GitHub Claw Machine) Ellie will be the first to tell you: attending a tech conference can be a total game-changer. ā€œAttending my first tech conference completely changed my career trajectory,ā€ she says. ā€œIt honestly changed my life.ā€ And the best part? You might not even need to pay for a ticket. > ā€œMost conferences offer scholarship tickets,ā€ Ellie explains. ā€œAnd if youā€™re active in dev communities, there are always giveaways. You just have to know where to look.ā€ In her early days of job hunting, Ellie made it to multiple conferences for free (minus travel and lodging)ā€”which she recommends to anyone trying to break into tech. Also, she lives for conference swag. One of her all-time favorite moments? Winning a GitHub Octocat from the claw machine at RenderATL. > ā€œSheā€™s one of my prized possessions,ā€ Ellie laughs. Proof here. šŸ™ Her advice: if youā€™re even a little curious about going to a conferenceā€”go. Show up. Say hi to someone new. You never know what connection might shape your next step. Ellieā€™s Journeys Away from her Desk Earlier this year, Ellie took a break from product launches and developer events to visit China for Chinese New Year with her boyfriendā€™s familyā€”and turned the trip into a mix of sightseeing, food adventures, and a personal mission: document every cat she met. (You can follow the full feline thread here šŸ±) The trip took them through Beijing, Nanjing, Taiyuan, Yuci, ZhĆ¹mĒŽdiĆ n, and Yangzhou, where they explored palaces, museums, and even soaked in a hot spring once reserved for emperors. > ā€œFancy, right?ā€ Ellie jokes. But the real highlight? The food. > ā€œChina has some of the best food in the world,ā€ she says. ā€œAnd lucky for me, my boyfriendā€™s dad is an amazing cookā€”every meal felt like a five-star experience.ā€ Whatā€™s Next? With a YouTube series on the way, thousands of developers reached through her workshops, and an eye on the next generation of AI tooling, Ellie Zubrowski is loving her experience as a developer advocate. Follow @elliezub on X to stay in the loop on her work, travels, tech experiments, and the occasional Octocat sighting. Sheā€™s building in public, cheering on other devs, and always down to share what sheā€™s learning along the way. Learn more about Pieces, the long-term LLM agent. Sticker Illustration by Jacob Ashley...

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co