Archive of

Technical Note: Testing ILogger with NSubstitute

1. The Challenge

Directly verifying ILogger extension methods (e.g., _logger.LogError("...")) with NSubstitute is difficult. These methods resolve to a single, complex generic method on the ILogger interface, making standard Received() calls verbose and brittle.

void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter);

2. The Solution: Inspect the State Argument

The most robust solution is to inspect the state argument passed to the core Log<TState> method. When using structured logging, this state object is an IEnumerable<KeyValuePair<string, object>> that contains the full context of the log call, including the original message template.

3. Implementation: A Reusable Helper Method

To avoid repeating complex verification logic, create a static extension method for ILogger<T>.

LoggerTestExtensions.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
using NSubstitute;
using FluentAssertions;

public static class LoggerTestExtensions
{
    /// <summary>
    /// Verifies that a log call with a specific level and message template was made.
    /// </summary>
    /// <typeparam name="T">The type of the logger's category.</typeparam>
    /// <param name="logger">The ILogger substitute.</param>
    /// <param name="expectedLogLevel">The expected log level (e.g., LogLevel.Error).</param>
    /// <param name="expectedMessageTemplate">The exact message template string to verify.</param>
    public static void VerifyLog<T>(this ILogger<T> logger, LogLevel expectedLogLevel, string expectedMessageTemplate)
    {
        // Find all calls to the core Log method with the specified LogLevel.
        var logCalls = logger.ReceivedCalls()
            .Where(call => call.GetMethodInfo().Name == "Log" &&
                           (LogLevel)call.GetArguments()[0]! == expectedLogLevel)
            .ToList();

        logCalls.Should().NotBeEmpty($"at least one log call with level {expectedLogLevel} was expected.");

        // Check if any of the found calls match the message template.
        var matchFound = logCalls.Any(call =>
        {
            var state = call.GetArguments()[2];
            if (state is not IEnumerable<KeyValuePair<string, object>> kvp) return false;
            
            return kvp.Any(p => p.Key == "{OriginalFormat}" && p.Value.ToString() == expectedMessageTemplate);
        });

        matchFound.Should().BeTrue($"a log call with the message template '{expectedMessageTemplate}' was expected but not found.");
    }
}

4. How to Use in a Unit Test

Step 1: Arrange In your test, create a substitute for ILogger<T> and inject it into your System Under Test (SUT).

// In your test class
private readonly ILogger<MyService> _logger;
private readonly MyService _sut;

public MyServiceTests()
{
    _logger = Substitute.For<ILogger<MyService>>();
    _sut = new MyService(_logger);
}

Step 2: Act Execute the method that is expected to produce a log entry.

[Fact]
public void DoWork_WhenErrorOccurs_ShouldLogError()
{
    // Act
    _sut.DoWorkThatFails();

    // ...
}

Step 3: Assert Use the VerifyLog extension method for a clean and readable assertion.

    // Assert
    _logger.VerifyLog(LogLevel.Error, "An error occurred while doing work for ID: {WorkId}");
}

5. Key Advantages of This Approach

  • Robustness: It verifies the intent (the message template) rather than the final formatted string, making it resilient to changes in parameter values.
  • Readability: The test assertion _logger.VerifyLog(...) is clean, concise, and clearly states what is being tested.
  • Reusability: The extension method can be used across the entire test suite.
  • Precision: It correctly targets the specific log level and message, avoiding ambiguity.

网友语录 - 第43期 - 相信别人是真的,相信别人是善的,就能为这个世界增加一些真和善

这里记录我的一周分享,通常在周六或周日发布。


碗君西木子 不要困在情绪牢笼里,always move on.


dust 很多以前的人所经历过的是后来人远远无法想象的。我有一个姑外祖母,抗战时代她十几岁,兵荒马乱家里太穷,就把她许给了个地主家当小老婆。她脾气好,一直逆来顺受,可是就在送她去夫家那天,她来了个大逃亡,不知道跑到哪里去了。她家就派人到处找,找了几天听熟人说看见她在某小镇讨饭。一伙人又追了过去,看见她和另外一个邻村的女孩一起躲在路边,她们看见人来了,撒腿就跑,一直跑到长江边。那个女孩不敢跑了,姑外祖母则纵身跳到江里。我外曾祖父也跳了下去,把她捞了上来。据说她回家的一路像一只不服但又无可奈何的小野兽,喉管里发咕咕的低吼。所有人都以为她疯了。后来她的事情传开了,人家也不要她了,她就一副神神叨叨的样子。再后来她嫁给了一个国军的小军官,这人天天打她,她夜夜鬼哭狼嚎。某一天军官突然跑了,她无儿无女,一个人守个破房子,饱一顿饥一顿。等我外曾祖父经济条件好转了点,看她可怜就把她接回了家。她有个小布袋子,视为珍宝,天天放在贴身衣物里,谁也不知道里面是啥。无数年过去了,她的精神状态时好时坏,外曾祖父去世后,照顾她的任务就交给了我外公。我小时候在外公家看到她当她是怪物,有点怕她,但她对我很友善,还拿过糖给我吃。有次我看见她偷偷捧着她的小袋子哭,就跟外公说,外公也不吱声。我就一直好奇,那袋子里到底是啥?2007年,我跟姑外祖母聊过天,她已经垂垂老矣,身体佝偻,像一棵马上就要断的老树。但她说话时却很平静,头脑比我预想的清醒。我说我能理解她当年为什么跑?她哭了出来。我就看着不说话。她哭了会儿,就从她的衣服里拿出并打开了那个袋子。里面是一张老照片,我一看,是她那年逃跑时和那个邻村的女孩合拍的照片。两个人脸上挂着微笑,都很漂亮,我一下就懂了。她看到我的眼神也知道我懂了。我没跟别人说过这事。过了几年她死的时候,葬礼上有人要拿走那个袋子,以为有什么钞票,我阻止了,最后就连她的人一起烧了。我现在有时还会想起那张照片,其实哪里有那么多怪异的人,都是被时代压变形的。我也很庆幸跟姑外祖母聊过,也许有一个人知道她的事,并且没有表示蔑视,对她来说这是一生中得到的唯一一次安慰


何帆: 听美国法官吐槽

与纽约皇后区刑事法院的法官交流时,我问他们,作为法官,内心对陪审团到底是何态度,是宁愿自己审,还是交陪审团定罪。一位老法官意味深长地回答:“这得看怎么讲了。说好听点,12个人的智慧,总比1个人的高明。说难听点,黑锅由12个人背,总比1个人背强。”


普法:对于权力的行使而言,法无明文授权即为禁止;对于权利的享有而言,法无明文禁止即允许


5 things a Stoic accepts early:

– Life isn’t fair – People are flawed – Pain is a teacher – Control is limited – Time is running out


小青 我发现了一件很奇妙的事情,我好像不需要问自己是什么情绪,我只需要问自己是不是在害怕就行了。每当我发现自己行为不对劲的时候,把“你应该”、“你必须”换成“你害怕吗?具体怕什么?”大多数时候就能准确把握自己的情绪。但是这样呢,又遇到另一个问题,就是当我的恐惧被自己看到的时候呢,我相信对我的长期身心健康有好处,但短期内我会立刻瘪嘴就哭……就要么是个内化了的糟糕父母,要么是个随地大哭的小孩子。我不知道怎么办才好。

我好像知道为什么我小时候我一哭我爸妈就打我了,因为他们不知道怎么应对一个哭泣、失控的孩子,我哭的时候,他们也不知道怎么办才好。他们没有“成功”过,他们只是看到那些他们见过的成功人士看起来都情绪稳定、勤奋坚强,不知道怎么成为和培养那样的人,就只能从表面上把我规定成那个样子。但我天生就不是一个“有种你打死我”的人,我从小对任何暴力都极其恐惧,看唐僧被妖精抓走我都换台。于是他们用不着下狠手打我,只要骂脏话、命令我自己去跪搓衣板就能轻易用恐惧控制我的行为。

有时候我觉得这种无所不在的恐惧不是我一个人的,而是我祖上很多代的农民和城镇平民写在基因里,我绝大多数的同胞写在文化和语言里的底噪


到爱尔兰的头两个星期,维特根斯坦住在都柏林的罗斯旅馆。只要医院里没事,德鲁利就陪维特根斯坦到都柏林城里或周边寻找可能的住处。没地方能提供他需要的孤独和平静, 但德鲁利在圣帕特里克医院的朋友罗伯特·麦卡洛夫暂时解决了这个问题。麦卡洛夫常去维克洛郡瑞德克洛斯的一处农舍度假,房子属于理查德·金斯顿和詹妮·金斯顿,他们对他说过想招一个永久房客。这个信息传给了维特根斯坦,他立刻从都柏林动身去“勘察现场(case the point)”(这时候他的词汇里包含了从美国侦探小说里借来的一点新鲜用语)。维克洛郡迷住了他。“坐公车前往的路上,”回来后他告诉德鲁利,“我不停地对自己说,真是个真正美丽的国度。”

不过,搬进金斯顿夫妇的农舍后没多久,他就写信对里斯说自己在那儿感到“冷和不舒服”: “我也许会在几个月之内搬到西爱尔兰的某个隔绝得多的地方。” 但几个星期后他适应多了, 德鲁利第一次去瑞德克洛斯时, 看起来一切都很好。维特根斯坦告诉他: “有时我的想法来得如此迅速,我觉得仿佛有什么在引导着我的笔。现在我清楚地看到,放弃教授职位是正确的。在剑桥我永远做不完这工作。”

维特根斯坦传


他说穆尔克里斯全家都是不想干任何活的人。他震惊地看到,虽是个出色的女裁缝,托米的母亲却衣衫褴褛地晃悠,托米自己虽是个合格的木匠,但他们屋子里的每一张椅子都有一条断腿。他在日记里径直说托米——“我在这儿完全依赖于他”——是“不可靠的”。

无论可不可靠,托米是他有的一切。他最近的邻居莫蒂默一家认为他完全疯了,不愿跟他有任何关系。他们甚至禁止他走进他们的地界,理由是他会吓坏他们的羊。因此,如果他想到罗斯洛后面的山岗上走走,就不得不走一条长而迂回的路线。有一次他这样散步时,莫蒂默家的人看到他突然停住,用手杖当工具在路上的泥地里画一个轮廓图(一个兔-鸭图?),他站着,长时间全神贯注盯着这张图,然后又走了起来。这事印证了他们最初的看法。还有一件事也是如此:一天晚上,莫蒂默家的狗叫声打乱了维特根斯坦的专注,他猛烈爆发了。事实上,他留给莫蒂默家的印象,与他先前留给奥地利乡下村民的印象颇为雷同。

托米也觉得维特根斯坦有点怪。但部分因为对德鲁利家的忠诚(迈尔斯·德鲁利曾跳下船救出了溺水的托米),部分因为开始喜欢“教授”的相伴,他愿意竭尽所能使维特根斯坦在罗斯洛的居住尽量舒适和愉快。例如,他尽了最大的努力满足维特根斯坦严格的清洁和卫生标准。按维特根斯坦的建议,他每天早晨不只送去牛奶和煤,还送去自己用过的茶叶。每天早晨,茶叶洒在厨房地板上吸污垢,然后扫掉。维特根斯坦还叫托米弄掉屋子里的“甲壳动物”(土鳖虫)。托米的做法是,给整个屋子喷了多到令人窒息的消毒粉。毕生害怕每一种虫子的维特根斯坦对结果感到满意,他情愿面对窒息的威胁,也不愿意看见土鳖虫。

罗斯洛农舍有两个房间, 一个卧室和一个厨房, 维特根斯坦的大部分时间在厨房里度过。但没用厨房做饭。在罗斯洛时,他几乎完全依靠从戈尔韦的一家杂货店里订购的罐头食品。托米挺担忧他的饮食。“罐头食品会吃死你”, 他有一次说。维特根斯坦的回答是阴森的: “反正人活得太久了”。维特根斯坦把厨房改作书房, 托米早晨去时, 常常发现他坐在厨房的桌子边, 往夹起来的散页上写着什么。几乎每天都有一堆丢掉的纸页, 烧掉它们是托米的活。

一天早晨,托米到罗斯洛时听见维特根斯坦的说话声, 进屋后惊讶地发现只有“教授”自个。“我以为你有个伴在这儿呢,”他说。“我是有,” 维特根斯坦回答, “我在跟我的一个很亲爱的朋友——我自己——谈话。” 在他这时期的一本笔记本里, 这句话得到了呼应:

几乎我的所有写作都是跟我自己的私人谈话。我跟自己促膝而谈的话。


在森林里看花晒太阳的小熊 过度苛求自己完美,以过高标准要求和评判自己是人生早期不健康环境下形成的求生策略与机制,是出于自我保护的原因形成的。 现在我们已经长大啦,拥有了更多的空间、选择权、能力和自由,不再随时受生存威胁控制和胁迫,就要适度地学习给自己松绑让自己放松,学会看清自己的正当性,表述和满足自己的需求,这对我们的塑造完整的人格以及创造幸福的生活有着无可替代的价值。

在森林里看花晒太阳的小熊 对害怕休息和停顿,会push自己不断前进思考的朋友说:

休息不是拒绝,而是为了更深的“接受"。 休息不是停顿,而是为了更好更有能量地出发

休息和战斗、开拓、创新、改革同等神圣。 你值得通过休息获得滋养与修复。

宇宙会为我们保留所有真正属于我们的机会。


王海鹏Seal:不认为自己对,就不会认为别人错。不认为自己知道,就不会认为别人无知。不认为自己智商高,就不会认为别人是白痴。


不管是哪一层的领导,在分配一件任务的时候,请尽可能做到让这件事情可评估。这既有利于任务接受者把事情保质保量的做好,也有助于你得到有效的反馈。


相信别人是真的,相信别人是善的,就能为这个世界增加一些真和善


小青 游记一定要三天内写出来,哪怕非常粗糙!!这么好看的塔我已经不记得是哪里拍的了……


我对夸大创造力的说辞持怀疑态度:我认为首先你需要以精确、技术、具体和现实感为基础。只有在某种平凡坚固的基础上才能产生创造力:幻想就像果酱,你必须把它涂在一片结实的面包上。如果没有,它仍然是一个无形的东西,你不能拿它做任何东西

—伊塔洛•卡尔维诺(Italo Calvino)


Geek

有点意思,居然还有这种脚本 《基于Debian搭建HomeNAS》将 Debian 系统快速配置成准 NAS 系统。

https://github.com/kekylin/debnas

轻松实现文件共享、照片备份、家庭影音、管理Docker、管理虚拟机、建立RAID等功能,使得Debian系统能够高效稳定地承担NAS任务。


Morris

人生不需要有意义,需要的是有意思。


陈同辉 说一下吧,普通人,没有背景的普通人,一定要知道,你个人价值的体现,必须在商业化的环境中才能实现。不要去抵制商业化,真正喜欢非商业化的,是有权的人,因为只有在非商业化环境,有权的人才可以以非常廉价的方式,调动、占用、劳役资源(包括劳动力资源),所以权力是天然反对商业化的。 而普通人,在非商业化的环境中,劳动力是一文不值的,这会导致劳动力的载体“生命”也是不值钱的。如果劳动力不值钱,生命不值钱,你所有的奋斗和勤奋,也一文不值,因为你所有的奋斗和勤奋的成果,会被权力无偿占有。 大家去想想,无论是从一个国家内部,还是不同国家之间对比,是不是如此? 再进一步,普通人,一定要去对资本友好的地方,具体就不展开了,这些东西,能懂的,自然想的明白。

Flutter iOS Safari Double Selection Issue - Technical Workaround

Problem

Flutter web apps on iOS Safari exhibit a "double selection" bug where text selection creates two overlapping selection layers, causing visual artifacts and interaction issues.

Root Cause

iOS Safari creates both native browser text selection AND Flutter's custom SelectionArea selection simultaneously, resulting in conflicting selection states.

Working Workaround

HTML Solution (web/index.html)

<head>
<style>
  * {
	-webkit-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;
	/* Disable caret to prevent selection artifacts */
	caret-color: rgba(255, 255, 255, 0) !important;
  }
</style>
</head>
<body oncontextmenu="event.preventDefault();" >
...

Flutter Integration

// In main.dart or app initialization
import 'package:flutter/gestures.dart';

void main() {
    // Disable browser context menu to let Flutter handle selection
    if (kIsWeb) {
        BrowserContextMenu.disableContextMenu();
    }
    runApp(MyApp());
}

How It Works

  1. Disables native selection - user-select: none prevents Safari's text selection
  2. Blocks touch events - Prevents iOS touch selection gestures
  3. Maintains Flutter selection - BrowserContextMenu.disableContextMenu() allows Flutter's SelectionArea to work
  4. Hides caret artifacts - caret-color: transparent eliminates visual glitches

Trade-offs

  • ❌ Breaks native web text selection outside Flutter widgets
  • ❌ May affect accessibility tools
  • ✅ Provides consistent cross-platform selection behavior
  • ✅ Eliminates iOS-specific double selection bug

Status

This workaround is recommended for Flutter web apps requiring text selection on iOS Safari until the official framework fix is released.

Git Aliases for Clean Commits

Today I'd love to introduce three useful Git aliases to maintain clean code and intelligent staging.

Quick Setup

# 1. Remove trailing whitespace from modified files
git config --global alias.cleanup '!git -c color.status=false status -s | grep -v "^D\|^.D" | cut -c4- | xargs -r -I {} sed -i "s/[[:space:]]*$//" {}'

# 2. Stage only substantive changes (ignore whitespace)  
git config --global alias.stagewhitespace '!git reset . && git add -N . && git diff -w -b | git apply --cached'

# 3. Combine both operations
git config --global alias.smartadd '!git cleanup && git stagewhitespace'

Usage

git cleanup         # Clean trailing spaces only
git stagewhitespace # Stage content changes only
git smartadd        # Clean + smart staging

What Each Alias Does

cleanup

  • Removes trailing whitespace from all modified files
  • Skips deleted files to avoid errors
  • Handles ANSI color codes in git status output

stagewhitespace

  • Resets staging area
  • Marks new files as intent-to-add
  • Stages only meaningful content changes
  • Ignores pure whitespace modifications

smartadd

  • Runs cleanup first, then stagewhitespace
  • One command for clean, intelligent staging

Key Features

  • Safe: Filters out deleted files to prevent errors
  • Smart: Handles new files correctly
  • Clean: Maintains consistent code formatting
  • Focused: Stages only substantive changes

Technical Details

The aliases handle several edge cases:

  • ANSI color codes in git output (disabled with -c color.status=false)
  • Deleted files that would cause sed errors (filtered with grep -v "^D\|^.D")
  • New untracked files (handled with git add -N)
  • Empty file lists (handled with xargs -r)

Perfect for teams that want clean commits without manual whitespace management.

网友语录 - 第42期 - 伟大成就需要的是以年为单位的积累,而不是每周都冲刺

这里记录我的一周分享,通常在周六发布。


梨梨原上草 @svuoalnalnis 公司把保洁外包给一间公司,常来的就那么几个面孔。时间长了,你就能体会她们之间的差别。

有时候一走进厨房,你就知道今天来上班的肯定是其中的某一个。而另外一些天,不用具体看脸也知道来摸鱼的是哪几个……

其实想对其中几个说,你们可能干啥都会做得很好,不限于保洁。只要时间允许,我的活也能干。


还真是这样。一生要是每本书都一字一句的读完,确实读不了几千本。但真正值得一字一句读完的书完全没有那么多,你需要略读上万本书(甚至可以更多)才能找出为数不多值得读一次又一次的那些书。一本书翻了几页放下,没什么,很多书没有读完,没什么。没有读完的那些书,书自己也有责任。


戚小诺 我问小九怎么表达他的愤怒,他说他的脑子里会有一个白色小人,一个黑色小人,黑色小人想要去干架:冲啊!我要干架!白色小人就在一边劝架:算了算了,不要打架,也许打不赢会受伤,爸爸妈妈还会担心。然后自己就会冷静很多。——头脑特工队没白看哈😂😂


memoryza 人们天生就是高效的改进者,而只有极少数会愿意从零开始


失败不可怕,一蹶不振才可怕

反馈是变得更好的关键。正如蓝迪波什所说:"人之所以会改变,是因为获得了反馈"。"失败"的本质是一种强反馈,它告诉我们这样或者此时不行,我们需要调整。在这个世界上,只要我们的生命还在继续(留得青山在),就没有真正的失败(不怕没柴烧),有的只是不断调整的过程。一个人的成长速度,取决于他得到反馈的频率和质量。因此,我们应该勇于改变并珍视每一次的反馈,正是它们引导着我们一步步实现人生目标。


真正重要的工作都不是一口气完成的,而是反复回到同一主题,慢慢打磨。像居里夫人发现放射性元素那年夏天,也去乡下休假、爬山。伟大成就需要的是以年为单位的积累,而不是每周都冲刺。别被高强度伪忙碌欺骗,真正的生产力,是以自然节奏、长期愿景,持续推进关键事务


生活若总是愿意吃苦,就有吃不完的苦;若总是抱怨生活,就有抱怨不完的生活。总觉得前路还长,却不知不觉就进入四十几五十几。能改善生活质量的钱一定要尽早花,早花早享受


调查期望不是让用户畅想这产品能具有什么功能,而是为了满足现有需求应该具有什么功能


LLM 的注意力其实是个滑动窗口,不持续提醒,很容易跑偏,这一点就跟我们管理一个想法很多的员工一个道理。(哎!人的注意力也是个滑动窗口。)