Test Automation (SDET) Engineer — Interview Questions
In short: Many fast unit tests at the bottom, fewer API/integration tests in the middle, and a small number of slow UI/E2E tests at the top.
- It gives fast feedback and stable suites: most logic is tested cheaply below the UI, while a few E2E tests confirm critical user journeys.
- The anti-pattern is the "ice-cream cone" (mostly UI tests) — slow and flaky.
In short: Automate stable, repetitive, high-value checks: regression, smoke, data-driven cases, API contracts, cross-browser critical paths.
- Don't automate exploratory and usability testing, one-off checks, features that change daily, or anything where the maintenance costs more than running it manually.
In short: Prefer stable, unique attributes: data-testid/data-qa → id → name → CSS selectors → relative XPath (text, attributes, axes) → link text.
- Avoid absolute XPath and generated class names.
- If the app lacks good attributes, ask developers to add test ids — it's the most effective fix for brittle tests.
In short: Implicit: a global timeout applied to every element lookup.
- Explicit:
WebDriverWaitwaiting for a specific condition (visible, clickable, text present) up to a timeout — recommended. - Fluent: an explicit wait with configurable polling and ignored exceptions.
- Never use
Thread.Sleep, and don't mix implicit and explicit waits (timeouts combine unpredictably).
In short: A design pattern where each page or component is a class that owns its locators and exposes user actions (LoginAs, AddToCart).
- Tests read like user steps, locators live in one place (one fix when the UI changes), and code is reused.
- Variants: Page Factory (Java), component objects, Screenplay pattern.
In short: Playwright for new web projects: auto-waiting and retrying assertions (less flakiness), fast isolated browser contexts, built-in tracing, network mocking, API testing, parallel runs and codegen.
- Selenium when you need the W3C standard, the widest language/browser support (including real Safari and legacy browsers), Appium for mobile, an existing large Selenium framework, or vendor grids.
- Both work well with C#.
In short: It means the element you hold was removed or re-rendered in the DOM.
- Re-locate the element right before using it (page-object properties that find each time, not cached fields), wait for the page update to finish (explicit wait on a condition), and include it in ignored exceptions of a fluent wait for dynamic grids.
In short: Language and runner matching the team (C# + NUnit/xUnit), Selenium or Playwright for UI and HttpClient/RestSharp for API, layered structure (tests → pages/components → core: driver factory, config, waits, logging → data builders → API helpers), configuration per environment, test data created via API, parallel execution, screenshots/traces and reports on failure, CI integration with categories (smoke/regression), and coding standards with code review.
- Optionally Reqnroll for BDD.
In short: Using HttpClient/RestSharp/Postman: send requests with correct and incorrect data, check status codes (200/201/204/400/401/403/404/409), response body and JSON schema, headers, error format, authentication and authorisation per role, boundary values, pagination and sorting, idempotency of PUT/DELETE, and response time.
- Chain requests (create → get → update → delete) and clean up data.
In short: One driver/browser context per test (thread-local or per-test instance), no static shared state, unique test data per test (GUIDs, separate users), tests independent of order, config for worker count, and infrastructure that can handle the load (Selenium Grid, cloud grid, Playwright workers).
- NUnit:
[Parallelizable]with instance-per-test-case; xUnit: collections control parallelism.
In short: Tests live in the same repo or a sibling repo, run via dotnet test in Azure DevOps/GitHub Actions/Jenkins: unit and API smoke on every PR (quality gate), deployment smoke tests after each environment deploy, full regression nightly.
- Headless browsers or a grid, results published (TRX/JUnit), artefacts on failure (screenshots, videos, traces), notifications, and failing tests block the pipeline.
In short: Measure first: list slowest and flakiest tests.
- Move logic checks down the pyramid (API/unit tests); keep UI tests for critical journeys.
- Replace sleeps with condition waits or Playwright auto-wait; use stable locators; create test data via API instead of UI steps; reuse login state.
- Run in parallel on a grid with independent data.
- Quarantine flaky tests and fix root causes.
- Target: smoke < 10 min per PR, full regression < 1 hour, flake rate < 1–2%.
In short: Compare environments: headless vs headed (viewport/screen size differs), browser/driver versions, timing on slower agents, environment URLs and config, test data present/missing, parallel runs colliding, time zone/culture, network/proxy.
- Collect evidence: screenshots, video, Playwright trace, browser console and network logs, CI logs.
- Reproduce with the same settings locally (headless, same browser version, same data).
A value type variable holds the data itself (int, bool, decimal, DateTime, any struct/enum). Assigning it copies the value. A reference type variable holds a reference to an object on the managed heap (class, string, arrays, delegates, record). Assigning it copies the reference, so two variables can point to the same object.
var a = new List<int>(); var b = a; b.Add(1); // a.Count == 1 — same object
int x = 1; int y = x; y++; // x is still 1Value types are stored inline (stack for locals, inside the parent object for fields); reference-type objects always live on the heap and are cleaned up by the GC.
string and StringBuilder?In short: string is immutable: every concatenation creates a new string object, so building a string in a loop causes many allocations (O(n²) copying).
StringBuilderkeeps a mutable buffer and appends in place; callToString()once at the end.- Use
StringBuilderfor loops or many appends, plain strings/interpolation for a handful of pieces.
const vs readonly vs static readonly.const: compile-time constant, implicitly static, only primitives/strings; the value is baked into callers at compile time.readonly: instance field assigned at declaration or in a constructor; can differ per object.static readonly: one value per type, computed at runtime (e.g.static readonly HttpClient Client = new()).
Gotcha: if library A has public const int Max = 10 and app B uses it, changing it to 20 in A does nothing in B until B is recompiled. Use static readonly for values that may change.
In short: Boxing converts a value type to object (or an interface) by allocating a heap object and copying the value; unboxing casts it back to the exact value type.
- It costs an allocation, a copy and GC pressure.
- Typical sources: non-generic collections (
ArrayList),string.Formatwith value-type arguments on old APIs, calling interface methods on structs via the interface. - Generics remove most boxing.
== and .Equals()?In short: For reference types, == compares references unless the operator is overloaded; Equals() is virtual and can be overridden to compare content.
stringoverloads both to compare text.- Records generate value-based
Equalsand==. - For value types,
Equalscompares content. - When you override
Equals, also overrideGetHashCode(objects that are equal must have the same hash code, or dictionaries break).
ref, out and in parameters?All three pass an argument by reference:
ref— must be initialised before the call; the method can read and change it.out— need not be initialised; the method must assign it (used byTryParse).in— read-only reference; avoids copying large structs.
decimal instead of double for money?In short: double is binary floating point and can't represent many base-10 fractions exactly (0.1 + 0.2 != 0.3), so totals drift by tiny amounts that show up in invoices and reports.
decimalstores base-10 digits exactly with 28–29 significant digits.- Trade-off:
decimalis slower and uses 16 bytes, so usedoublefor scientific/graphics maths anddecimalfor money and quantities that must add up exactly. - In SQL Server map it to
DECIMAL(18,2), notFLOAT.
Parse, TryParse and Convert?In short: int.Parse("x") throws FormatException for bad input (and ArgumentNullException for null).
int.TryParse(s, out var n)returnsfalseinstead of throwing — use it for user input.Convert.ToInt32(value)accepts many types (object, double, string), returns 0 fornull, and rounds doubles to the nearest even number (Convert.ToInt32(2.5)→ 2) whereas a cast truncates ((int)2.9→ 2).- Pass a
CultureInfowhen parsing numbers and dates from files or APIs.
Dictionary and Hashtable?In short: Dictionary<TKey,TValue> is generic: type-safe, no boxing, faster; a missing key with the indexer throws KeyNotFoundException (use TryGetValue).
Hashtableis the old non-generic version storingobjectkeys/values: needs casts, boxes value types, and returnsnullfor a missing key.- Both are hash tables with O(1) average lookup.
- For multiple threads use
ConcurrentDictionary.
In short: A multidimensional (rectangular) array int[,] is one object where every row has the same length, accessed as a[r, c].
- A jagged array
int[][]is an array of arrays — each row is a separate array and can have a different length, accessed asa[r][c]. - Jagged arrays are more flexible and often faster in .NET; rectangular arrays suit fixed grids such as matrices.
IEnumerable, ICollection, IList and IQueryable.IEnumerable<T>: forward-only iteration (foreach), deferred execution with LINQ to Objects, runs in memory.ICollection<T>: addsCount,Add,Remove,Contains.IList<T>: adds index access andInsert.IQueryable<T>: builds an expression tree that a provider (EF Core) translates to SQL and runs in the database.
db.Orders.Where(o => o.Total > 100).ToList(); // IQueryable: WHERE in SQL
db.Orders.AsEnumerable().Where(o => o.Total > 100).ToList(); // loads ALL rows then filters in memoryIn short: A delegate is a type-safe function pointer (Func, Action, custom delegate).
- It can be multicast.
- An event wraps a delegate so outside code can only
+=/-=handlers; only the declaring class can raise it. - This prevents subscribers from wiping out other handlers (
= null) or raising the event themselves. - Pattern: `public event EventHandler
? - Changed;
and raise withChanged?.Invoke(this, args)`.
In short: No new thread is created by await.
- The compiler turns the method into a state machine.
- When it hits
awaiton an incomplete task, the method returns to its caller and the thread goes back to the pool. - When the I/O completes, a continuation resumes the state machine — on the captured synchronisation context if there is one (UI thread), otherwise on a thread-pool thread.
- CPU-bound work still needs
Task.Runor parallelism to move off the current thread.
Task.WhenAll and awaiting tasks one by one?In short: Awaiting sequentially makes total time the sum of the calls; starting all tasks and then await Task.WhenAll(...) makes it roughly the longest single call.
WhenAllthrows the first exception when awaited, but the returned task'sExceptionholds all of them.- Use
WhenAnyfor "first to finish" or timeouts.
async void dangerous?In short: Callers can't await it, so they can't know when it finishes, and an exception thrown inside crashes the process (it is raised on the synchronisation context instead of being stored in a Task).
- Use
async Taskeverywhere except event handlers.
IDisposable and the using statement? Dispose vs Finalize?In short: IDisposable.Dispose() releases unmanaged resources (connections, file handles) deterministically.
usingguaranteesDisposeis called even if an exception is thrown (compiles totry/finally).- A finaliser is called by the GC at an unpredictable time and costs an extra GC cycle; it is only a safety net for raw unmanaged handles.
- In
DisposecallGC.SuppressFinalize(this)if a finaliser exists. - Most code just wraps
SafeHandleand never writes a finaliser.
In short: The heap is split into Gen 0 (new objects, collected very often and cheaply), Gen 1 (survivors of Gen 0, a buffer), and Gen 2 (long-lived objects, collected rarely and expensively).
- Most objects die young, so collecting Gen 0 frequently is efficient.
- Objects of 85,000 bytes or more go to the Large Object Heap, collected with Gen 2. .NET has workstation and server GC modes, plus background (concurrent) GC for Gen 2.
In short: Derive from Exception, end the name with Exception, provide the standard constructors (parameterless, message, message + inner exception) plus constructors/properties for useful data (e.g. Balance, Requested).
- Create one only when callers need to catch that specific situation (e.g.
InsufficientFundsException→ show a friendly message or return 409); - otherwise throw the built-in ones (
ArgumentException,InvalidOperationException). - Always pass the inner exception when wrapping.
lock(sugar forMonitor.Enter/Exit): mutual exclusion inside one process, synchronous only.SemaphoreSlim: limits concurrency to N and supportsWaitAsync, so use it when you mustawaitinside the critical section.Interlocked: atomic operations on single variables (increment, compare-exchange), lock-free.ConcurrentDictionary/ConcurrentQueue/Channel<T>: built-in thread-safe structures.- Immutability: data that never changes needs no locks.
- Across processes/servers: database locks, distributed locks (Redis, blob leases),
Mutexfor the same machine.
In short: Classic case: in a UI app or old ASP.NET (with a synchronisation context), code calls task.Result.
- The blocked thread holds the context; the awaited method's continuation needs the same context to resume, so both wait forever.
- Fixes: go async all the way (
awaitinstead of.Result), useConfigureAwait(false)in library code, and never mix blocking and async calls. - ASP.NET Core has no synchronisation context, but blocking still causes thread-pool starvation under load.
+= on a string and takes minutes. What do you do?In short: String concatenation in a loop copies the whole string every time, so the work is O(n²).
- Replace it with a
StringBuilder, or better, stream the rows straight to the output (StreamWriter, orPipeWriterin ASP.NET Core) so the full file is never held in memory. - Measure before and after with a stopwatch or BenchmarkDotNet.
- For huge exports, page the database query and write as you read.
- Confirm the trend with
dotnet-counters(GC heap size, Gen 2 count, LOH size). - Take memory dumps a few minutes apart with
dotnet-dump collect(ordotnet-gcdump) and compare which types grow, using Visual Studio, PerfView ordotnet-dump analyze(dumpheap -stat,gcroot). - Usual culprits: static collections or caches with no eviction, event handlers never unsubscribed,
HttpClientcreated per request, undisposed streams/DbContexts, large byte arrays on the LOH, unboundedMemoryCache. - Fix (size-limited cache,
IHttpClientFactory,using, weak events), then load-test to confirm.
In short: count++ is read, add, write — three steps.
- Two threads can read the same value and both write value+1, so one increment is lost (a race condition).
- Fix with
Interlocked.Increment(ref count), alock, or a concurrent type. - If the counter must survive restarts or work across several servers, keep it in the database (`UPDATE ...
- SET Count = Count + 1
) or in RedisINCR` instead.
- Encapsulation — private state with a controlled public API.
BankAccountkeeps_balanceprivate and exposesDeposit()that rejects negative amounts. - Abstraction — expose what, hide how.
IPaymentGateway.ChargeAsync()hides HTTP calls, signatures and retries. - Inheritance — a derived class reuses and extends a base.
Contractor : EmployeereusesNameandDescribe(). - Polymorphism — the same call behaves differently by type.
employee.MonthlyPay()runs thePermanentorContractoroverride at runtime.
In short: A class is a type definition (blueprint) describing fields, properties and methods; it exists in code and metadata.
- An object is an instance of that class created at runtime with
new, occupying memory with its own state. - One class, many objects.
| Overloading | Overriding |
|---|---|
| Same name, different parameters | Same signature in a derived class |
| Same class (or derived) | Requires virtual/abstract + override |
| Resolved at compile time | Resolved at runtime by the object's type |
| Return type alone can't differ | Return type the same (covariant returns allowed since C# 9) |
In short: An abstract class can hold state, constructors and shared implementation, and a class can inherit only one.
- An interface is a contract; a class can implement many, and it has no instance state (default methods are allowed since C# 8).
- Use an interface for capabilities and dependency injection (
IEmailSender) and an abstract class when related types share code and state (Shape,Employee). - A common design is to use both: the interface for consumers, an abstract base for shared code.