-
Notifications
You must be signed in to change notification settings - Fork 100
Testing high level API
⚠️ Alpha API —tik4net.testingis in early development. The builder methods and class names may change in future releases.
Package:
tik4net.testing(NuGet) — add it to your test project only.
See also: Testing low-level API · Testing mid-level API
TikFakeConnection intercepts at CallCommandSync so the full O/R mapper stack — including
TikEntityMetadata reflection, property mapping, boolean/enum conversion, and proplist handling —
runs for real. You get accurate coverage of your entity definitions without a live router.
using tik4net;
using tik4net.Testing;
using tik4net.Objects; // extension methods: LoadAll, Save, Delete, …
using tik4net.Objects.Ip; // your entity namespaceMany entity properties are read-only (private setter) because the router sets them — .id,
dynamic, actual-interface, etc. The TikFakeEntityExtensions class provides fluent helpers
to set these in test setup without raw reflection boilerplate.
Convenience shortcut for .WithValue(".id", id):
var entry = new FirewallAddressList { List = "BLACKLIST", Address = "10.0.0.1" }
.WithId("*1");
Assert.AreEqual("*1", entry.Id);Sets any property by its MikroTik field name (the string in [TikProperty("...")]).
Pass the typed C# value — not the wire string — so booleans are true/false, not "yes"/"no":
var addr = new IpAddress { Address = "10.0.0.1/24", Interface = "ether1" }
.WithId("*1")
.WithValue("dynamic", true)
.WithValue("actual-interface", "ether1");If the field name does not exist on the entity, ArgumentException is thrown with a list of
available field names to help diagnose typos.
WithEntities<TEntity> matches the entity's load command (/ip/address/print etc.) and
auto-serializes the supplied objects through TikEntityMetadata — the same metadata used when
reading from a real router:
var conn = new TikFakeConnection()
.WithEntities(
new IpAddress { Id = "*1", Address = "10.0.0.1/24", Interface = "ether1", Disabled = false },
new IpAddress { Id = "*2", Address = "10.0.0.2/24", Interface = "ether2", Disabled = true });
IEnumerable<IpAddress> addresses = conn.LoadAll<IpAddress>();
Assert.AreEqual(2, addresses.Count());
Assert.AreEqual("10.0.0.1/24", addresses.First().Address);
Assert.IsFalse(addresses.First().Disabled);var conn = new TikFakeConnection()
.WithEntities(new SystemIdentity { Name = "MyRouter" }); // singleton entity
SystemIdentity identity = conn.LoadSingle<SystemIdentity>();
Assert.AreEqual("MyRouter", identity.Name);// LoadById calls /print with ?.id=*1 — register both /print and the filtered response
var conn = new TikFakeConnection()
.WithEntities(new IpAddress { Id = "*1", Address = "10.0.0.1/24", Interface = "ether1" });
IpAddress addr = conn.LoadById<IpAddress>("*1");
Assert.AreEqual("*1", addr.Id);Save on a new entity (empty Id) calls /add → ExecuteScalar to get the new id.
Register WithScalarResponse for the /add command:
var conn = new TikFakeConnection()
.WithScalarResponse(rows => rows.First() == "/ip/address/add", "*5");
var newAddr = new IpAddress { Address = "192.168.88.1/24", Interface = "ether1" };
conn.Save(newAddr);
Assert.AreEqual("*5", newAddr.Id); // id written back by Save
conn.AssertWasSent("/ip/address/add");Save on an existing entity calls /print (to diff fields), then /set.
Register both:
var existing = new IpAddress { Id = "*1", Address = "10.0.0.1/24", Interface = "ether1", Disabled = false };
var conn = new TikFakeConnection()
.WithEntities(existing) // /print
.WithNonQuery(rows => rows.First() == "/ip/address/set"); // /set
existing.Disabled = true;
conn.Save(existing);
conn.AssertWasSent("/ip/address/set");
conn.AssertWasSent(rows => rows.Any(r => r.Contains("disabled=yes")));var conn = new TikFakeConnection()
.WithNonQuery(rows => rows.First() == "/ip/address/remove");
var addr = new IpAddress { Id = "*3", Address = "10.0.0.3/24" };
conn.Delete(addr);
conn.AssertWasSent("/ip/address/remove");var original = new List<IpAddress>
{
new IpAddress { Id = "*1", Address = "10.0.0.1/24", Interface = "ether1" },
new IpAddress { Id = "*2", Address = "10.0.0.2/24", Interface = "ether2" },
};
var backup = original.CloneEntityList();
// Modify: remove *2, add a new one
original.RemoveAt(1);
original.Add(new IpAddress { Address = "10.0.0.99/24", Interface = "ether3" });
var conn = new TikFakeConnection()
.WithNonQuery(rows => rows.First() == "/ip/address/remove")
.WithScalarResponse(rows => rows.First() == "/ip/address/add", "*10");
conn.SaveListDifferences(original, backup);
conn.AssertWasSent("/ip/address/remove"); // *2 deleted
conn.AssertWasSent("/ip/address/add"); // new entry addedvar received = new List<string>();
var done = new ManualResetEventSlim(false);
var conn = new TikFakeConnection()
.WithEntities(
new ToolTorch { TxBps = 1000, RxBps = 500 },
new ToolTorch { TxBps = 2000, RxBps = 800 });
ITikCommand torchCmd = conn.LoadAsync<ToolTorch>(
onLoadItemCallback: item => received.Add(item.TxBps.ToString()),
onExceptionCallback: error => Assert.Fail(error.Message),
conn.CreateParameter("interface", "ether1"),
conn.CreateParameter("port", "any"));
done.Wait(TimeSpan.FromSeconds(2));
torchCmd.CancelAndJoin();
CollectionAssert.AreEqual(new[] { "1000", "2000" }, received);When you need the fake to reflect changes made by the code under test (load → save → load again):
var store = new List<QueueSimple>
{
new QueueSimple { Id = "*1", Name = "Q1", MaxLimit = "10M/10M" },
};
var conn = new TikFakeConnection()
.WithEntities<QueueSimple>(() => store) // dynamic: re-evaluated on each call
.WithScalarResponse(rows => rows.First() == "/queue/simple/add", _ =>
{
var newId = "*" + (store.Count + 1);
store.Add(new QueueSimple { Id = newId, Name = "Q2", MaxLimit = "5M/5M" });
return newId;
})
.WithNonQuery(rows => rows.First() == "/queue/simple/set");
// First load — one queue
Assert.AreEqual(1, conn.LoadAll<QueueSimple>().Count());
// Add a new one
conn.Save(new QueueSimple { Name = "Q2", MaxLimit = "5M/5M" });
// Second load — two queues
Assert.AreEqual(2, conn.LoadAll<QueueSimple>().Count());// Was the right command sent?
conn.AssertWasSent("/ip/address/add");
// Was a specific parameter included?
conn.AssertWasSent(rows => rows.Any(r => r.Contains("interface=ether1")));
// How many times was /set called?
Assert.AreEqual(1, conn.GetCallCount("/ip/address/set"));
// Full history for custom assertions
foreach (var cmdRows in conn.SentCommands)
Console.WriteLine(string.Join(" | ", cmdRows));