# Testing search by meaning — TestEmbedder.Use and a deterministic embedder

> `TestEmbedder.Use(embedder)` makes a test run embed with an embedder written in Osy# — any class implementing `Osysharp.Memory.IEmbedder` — so a suite can assert what `Memory.Search` and `[Searchable(Memory)]` find without calling a model. `Osysharp.Memory.Testing` ships one: `WordsEmbedder`, free, offline and the same on every machine.

<!-- id: testing-test-embedder · area: testing · stability: preview · html: https://osysharp.com/reference/testing/test-embedder/ -->

## Summary        {#summary}

A field marked `[Searchable(Memory)]` is found by meaning once the app has an embedder. In a test there is no model to
call — and there should not be: a real embedder costs money, needs a key, and answers slightly differently from one
release to the next. **`TestEmbedder.Use`** hands the test run an embedder of your own instead, so every row the test
writes is embedded the moment it is committed, and `Memory.Search` on the next line finds it.

```osy syntax
[TestFixture]
void Kitchen() {
  TestEmbedder.Use(new WordsEmbedder());   // Osysharp.Memory.Testing
  new Recipe { Title = "Lentil soup", Method = "Simmer red lentils with cumin until soft." };
}
```

## Signature      {#signature}

```osy syntax
TestEmbedder.Use(IEmbedder embedder);      // in a [Test] or [TestFixture] only

// Osysharp.Memory — what an embedder is
public interface IEmbedder {
  string Model();                                     // recorded with each vector
  List<double> Embed(string text, int dimensions);    // `dimensions` numbers, the app's vector width
}

// Osysharp.Memory.Testing — a hashed bag of words
public class WordsEmbedder : IEmbedder {
  public int MinLength = 2;                 // shorter words count for nothing
  public List<string> Ignore = [ … ];       // "the", "and", "of" … — the commonest English words
  public List<string> WordsOf(string text); // the words a text is embedded by
}
```

## Description    {#description}

### What it changes   {#what}
For the rest of the test (a fixture's call reaches every test that declares it):

- **Every searchable row is embedded at commit**, with your embedder, instead of by the background worker an app uses
  in production. A row written on one line is found on the next.
- **`Memory.Search` embeds its query** with the same embedder, so the two are comparable.
- **Nothing is metered.** A test embedder is not a model call: no budget is spent and no key is read.

The width is the app's own (`app.Embedding.Dimensions`, or the platform's default), passed to `Embed` as
`dimensions` — an embedder that answers a different number of values is refused with a message saying so.

### What `WordsEmbedder` finds   {#words}
It turns a text into its words (lower-cased, without the commonest ones), hashes each word into one of `dimensions`
slots, and scales the result to length one. **Two texts are as close as the words they share.** That makes it lexical,
not semantic — "invoice" is not near "bill" — so a test written against it searches with words its rows contain, and
asserts the things that matter about a search: **which rows are found, in which order, and which are never found**
(a row the searcher may not read, a draft, something erased).

To test meaning across words or languages, write an embedder that knows them — any class implementing `IEmbedder`
will do, and a small table of "these words mean the same" is often all a test needs.

### Why it is refused outside a test   {#test-only}
An embedder decides what a search finds. In app code, `TestEmbedder.Use` would let one line replace the app's real
embedder for everyone. So it is a compile error anywhere but a `[Test]` or `[TestFixture]`, and the platform refuses it
on a run that is not a test, saying so. The app needs `use Osysharp.Memory;` for it to resolve at all.

## Examples       {#examples}

A recipe book searched by what each method says — and a private recipe its owner's friends never find:

```osy title="the model — recipes, some shared and some kept" test app=testing-test-embedder
app RecipeBox {
  use Osysharp.Memory;
  use Osysharp.Memory.Testing@0;
  model "model/**/*.osy";
}

using Osysharp.Memory;
using Osysharp.Memory.Testing;

[Principal]
entity Cook {
  [Required, Unique, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

entity Recipe {
  [Required, MaxLength(120)] string Title;
  [Searchable(Memory), MaxLength(4000)] string Method;
  Cook? KeptBy;
  security {
    allow read where KeptBy == null || KeptBy == user;
    allow create when IsAuthenticated;
  }
}

public List<string> RecipesAbout(string words) {
  List<string> titles = [];
  foreach (var hit in Memory.Search(words, of: [Recipe], limit: 5)) {
    var r = Recipe.FirstOrDefault(x => x.Id == hit.SourceEntityId);
    if (r != null && !titles.Contains(r.Title)) { titles.Add(r.Title); }
  }
  return titles;
}

[TestFixture]
void Kitchen() {
  TestEmbedder.Use(new WordsEmbedder());
  var mia = new Cook { Email = "mia@kitchen.test" };
  new Cook { Email = "leo@kitchen.test" };
  new Recipe { Title = "Lentil soup", Method = "Simmer red lentils with cumin and onion until soft, then blend." };
  new Recipe { Title = "Flatbread", Method = "Knead flour, yoghurt and salt; rest the dough, then cook in a dry pan." };
  new Recipe { Title = "Grandma's stew", Method = "Brown the beef, add lentils and red wine, simmer three hours.", KeptBy = mia };
}

principal Mia => Cook.Single(c => c.Email == "mia@kitchen.test");
principal Leo => Cook.Single(c => c.Email == "leo@kitchen.test");

[Test(Kitchen)]
[runas(Leo)]
void a_recipe_is_found_by_what_its_method_says() {
  Assert.Equal("Lentil soup", RecipesAbout("simmer red lentils cumin")[0]);
}

[Test(Kitchen)]
void a_kept_recipe_is_found_only_by_its_cook() {
  runas (Leo) { Assert.False(RecipesAbout("beef lentils red wine simmer").Contains("Grandma's stew")); }
  runas (Mia) { Assert.Equal("Grandma's stew", RecipesAbout("beef lentils red wine simmer")[0]); }
}
```

## See also       {#see-also}

- [using Memory (semantic search)](https://osysharp.com/reference/memory/search/) — what the tests above call
- [[Searchable]](https://osysharp.com/reference/memory/searchable/) — marking a field findable
- [Stubbing your own functions in a test (fault injection)](https://osysharp.com/reference/testing/function-stubs/) — answering for your own functions in a test
- [Outbound calls in a test](https://osysharp.com/reference/testing/outbound-calls/) — answering for an HTTP call (`Http.Post.Stub`)
- [Testing model calls without a model — ScriptedModel (Llm kit)](https://osysharp.com/reference/agent/llm-scripted-model/) — answering for a model call
