Testing
The TestFactory class provides static factory methods for creating domain type instances with sensible defaults. Every parameter is optional -- specify only what your test cares about, and let the factory fill in the rest.
TestFactory lives in the FSharp.ATProto.Bluesky namespace. Add a reference to FSharp.ATProto.Bluesky in your test project.
Basic Usage
open FSharp.ATProto.Bluesky
// Minimal -- all defaults
let post = TestFactory.TimelinePost()
// Override specific fields
let post2 = TestFactory.TimelinePost(text = "Hello world", likeCount = 42L)
// Create a profile
let profile = TestFactory.ProfileSummary(displayName = "Alice")
Default values are deterministic: the default DID is did:plc:testfactory, the default handle is test.bsky.social, and so on. This keeps test output stable.
Available Factory Methods
PostRef
TestFactory.PostRef(?uri: AtUri, ?cid: Cid) : PostRef
ProfileSummary
TestFactory.ProfileSummary(?did: Did, ?handle: Handle, ?displayName: string, ?avatar: string) : ProfileSummary
Profile
TestFactory.Profile(
?did, ?handle, ?displayName, ?description, ?avatar, ?banner,
?postsCount, ?followersCount, ?followsCount,
?isFollowing, ?isFollowedBy, ?isBlocking, ?isBlockedBy, ?isMuted) : Profile
TimelinePost
TestFactory.TimelinePost(
?uri, ?cid, ?author, ?text, ?facets,
?likeCount, ?repostCount, ?replyCount, ?quoteCount,
?indexedAt, ?isLiked, ?isReposted, ?isBookmarked) : TimelinePost
Ref Types
TestFactory.LikeRef(?uri: AtUri) : LikeRef
TestFactory.RepostRef(?uri: AtUri) : RepostRef
TestFactory.FollowRef(?uri: AtUri) : FollowRef
TestFactory.BlockRef(?uri: AtUri) : BlockRef
FeedItem and Notification
TestFactory.FeedItem(?post: TimelinePost, ?reason: FeedReason) : FeedItem
TestFactory.Notification(
?kind: NotificationKind, ?author: ProfileSummary,
?subjectUri: AtUri, ?isRead: bool, ?indexedAt: DateTimeOffset) : Notification
Example: Testing a Filter Function
open FSharp.ATProto.Bluesky
open Expecto
let filterTests = testList "post filter" [
test "filters posts with high like count" {
let posts = [
TestFactory.TimelinePost(text = "Popular", likeCount = 100L)
TestFactory.TimelinePost(text = "Unpopular", likeCount = 2L)
TestFactory.TimelinePost(text = "Medium", likeCount = 50L)
]
let popular = posts |> List.filter (fun p -> p.LikeCount >= 50L)
Expect.equal popular.Length 2 "Should find 2 popular posts"
}
test "groups notifications by author" {
let alice = TestFactory.ProfileSummary(displayName = "Alice")
let bob = TestFactory.ProfileSummary(displayName = "Bob")
let notifications = [
TestFactory.Notification(content = NotificationContent.Like(TestFactory.PostRef()), author = alice)
TestFactory.Notification(content = NotificationContent.Follow, author = bob)
TestFactory.Notification(content = NotificationContent.Repost(TestFactory.PostRef()), author = alice)
]
let byAuthor = notifications |> List.groupBy (fun n -> n.Author.DisplayName)
Expect.equal byAuthor.Length 2 "Should have 2 authors"
}
]
Example: Testing Undo Logic
let undoTests = test "undo references have valid URIs" {
let likeRef = TestFactory.LikeRef()
let followRef = TestFactory.FollowRef()
// Default URIs contain the correct collection
Expect.stringContains
(AtUri.value likeRef.Uri)
"app.bsky.feed.like"
"LikeRef URI should reference the like collection"
Expect.stringContains
(AtUri.value followRef.Uri)
"app.bsky.graph.follow"
"FollowRef URI should reference the follow collection"
}
Multiple items
namespace FSharp
--------------------
namespace Microsoft.FSharp
namespace FSharp
--------------------
namespace Microsoft.FSharp
namespace FSharp.ATProto
namespace FSharp.ATProto.Syntax
namespace FSharp.ATProto.Core
namespace FSharp.ATProto.Bluesky
val post: TimelinePost
type TestFactory =
static member BlockRef: ?uri: AtUri -> BlockRef
static member FeedItem: ?post: TimelinePost * ?reason: FeedReason * ?replyParent: TimelinePost -> FeedItem
static member FollowRef: ?uri: AtUri -> FollowRef
static member LikeRef: ?uri: AtUri -> LikeRef
static member Notification: ?recordUri: AtUri * ?author: ProfileSummary * ?content: NotificationContent * ?isRead: bool * ?indexedAt: DateTimeOffset -> Notification
static member PostRef: ?uri: AtUri * ?cid: Cid -> PostRef
static member Profile: ?did: Did * ?handle: Handle * ?displayName: string * ?description: string * ?avatar: string * ?banner: string * ?postsCount: int64 * ?followersCount: int64 * ?followsCount: int64 * ?isFollowing: bool * ?isFollowedBy: bool * ?isBlocking: bool * ?isBlockedBy: bool * ?isMuted: bool -> Profile
static member ProfileSummary: ?did: Did * ?handle: Handle * ?displayName: string * ?avatar: string -> ProfileSummary
static member RepostRef: ?uri: AtUri -> RepostRef
static member TimelinePost: ?uri: AtUri * ?cid: Cid * ?author: ProfileSummary * ?text: string * ?facets: Facet list * ?likeCount: int64 * ?repostCount: int64 * ?replyCount: int64 * ?quoteCount: int64 * ?indexedAt: DateTimeOffset * ?isLiked: bool * ?isReposted: bool * ?isBookmarked: bool -> TimelinePost
<summary> Factory methods for creating domain objects with sensible defaults. Useful for testing consumers of the library without needing a live API. All parameters are optional; unspecified fields use deterministic default values. </summary>
<summary> Factory methods for creating domain objects with sensible defaults. Useful for testing consumers of the library without needing a live API. All parameters are optional; unspecified fields use deterministic default values. </summary>
static member TestFactory.TimelinePost: ?uri: AtUri * ?cid: Cid * ?author: ProfileSummary * ?text: string * ?facets: AppBskyRichtext.Facet.Facet list * ?likeCount: int64 * ?repostCount: int64 * ?replyCount: int64 * ?quoteCount: int64 * ?indexedAt: System.DateTimeOffset * ?isLiked: bool * ?isReposted: bool * ?isBookmarked: bool -> TimelinePost
val post2: TimelinePost
val profile: ProfileSummary
static member TestFactory.ProfileSummary: ?did: Did * ?handle: Handle * ?displayName: string * ?avatar: string -> ProfileSummary
Multiple items
val string: value: 'T -> string
--------------------
type string = System.String
val string: value: 'T -> string
--------------------
type string = System.String
type bool = System.Boolean
namespace Expecto
val filterTests: Test
val testList: name: string -> tests: Test list -> Test
<summary> Builds a list/group of tests that will be ignored by Expecto if exists focused tests and none of the parents is focused </summary>
<summary> Builds a list/group of tests that will be ignored by Expecto if exists focused tests and none of the parents is focused </summary>
val test: name: string -> TestCaseBuilder
<summary> Builds a test case </summary>
<summary> Builds a test case </summary>
val posts: TimelinePost list
val popular: TimelinePost list
Multiple items
module List from Microsoft.FSharp.Collections
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T with get member IsEmpty: bool with get member Item: index: int -> 'T with get ...
module List from Microsoft.FSharp.Collections
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T with get member IsEmpty: bool with get member Item: index: int -> 'T with get ...
val filter: predicate: ('T -> bool) -> list: 'T list -> 'T list
val p: TimelinePost
TimelinePost.LikeCount: int64
module Expect
from Expecto
<summary> A module for specifying what you expect from the values generated by your tests. </summary>
<summary> A module for specifying what you expect from the values generated by your tests. </summary>
val equal: actual: 'a -> expected: 'a -> message: string -> unit (requires equality)
<summary> Expects the two values to equal each other. </summary>
<summary> Expects the two values to equal each other. </summary>
property List.Length: int with get
val alice: ProfileSummary
val bob: ProfileSummary
val notifications: Notification list
static member TestFactory.Notification: ?recordUri: AtUri * ?author: ProfileSummary * ?content: NotificationContent * ?isRead: bool * ?indexedAt: System.DateTimeOffset -> Notification
type NotificationContent =
| Like of post: PostRef
| Repost of post: PostRef
| Follow
| Reply of text: string * inReplyTo: PostRef
| Mention of text: string
| Quote of text: string * quotedPost: PostRef
| StarterpackJoined of starterPackUri: AtUri
| Unknown of reason: string
<summary>The content of a notification, varying by kind.</summary>
<summary>The content of a notification, varying by kind.</summary>
union case NotificationContent.Like: post: PostRef -> NotificationContent
static member TestFactory.PostRef: ?uri: AtUri * ?cid: Cid -> PostRef
union case NotificationContent.Follow: NotificationContent
union case NotificationContent.Repost: post: PostRef -> NotificationContent
val byAuthor: (string * Notification list) list
val groupBy: projection: ('T -> 'Key) -> list: 'T list -> ('Key * 'T list) list (requires equality)
val n: Notification
Notification.Author: ProfileSummary
ProfileSummary.DisplayName: string
val undoTests: Test
val likeRef: LikeRef
static member TestFactory.LikeRef: ?uri: AtUri -> LikeRef
val followRef: FollowRef
static member TestFactory.FollowRef: ?uri: AtUri -> FollowRef
val stringContains: subject: string -> substring: string -> message: string -> unit
<summary> Expect the string `subject` to contain `substring` as part of itself. If it does not, then fail with `message` and `subject` and `substring` as part of the error message. </summary>
<summary> Expect the string `subject` to contain `substring` as part of itself. If it does not, then fail with `message` and `subject` and `substring` as part of the error message. </summary>
Multiple items
module AtUri from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="AtUri" /> values. </summary>
--------------------
type AtUri = private | AtUri of string override ToString: unit -> string
<summary> An AT-URI that identifies a resource in the AT Protocol network. AT-URIs use the scheme <c>at://</c> followed by an authority (DID or handle), an optional collection (NSID), and an optional record key. Format: <c>at://<authority>[/<collection>[/<rkey>]]</c>. Maximum length is 8192 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/at-uri-scheme </remarks>
module AtUri from FSharp.ATProto.Syntax
<summary> Functions for creating, validating, and extracting data from <see cref="AtUri" /> values. </summary>
--------------------
type AtUri = private | AtUri of string override ToString: unit -> string
<summary> An AT-URI that identifies a resource in the AT Protocol network. AT-URIs use the scheme <c>at://</c> followed by an authority (DID or handle), an optional collection (NSID), and an optional record key. Format: <c>at://<authority>[/<collection>[/<rkey>]]</c>. Maximum length is 8192 characters. </summary>
<remarks> See the AT Protocol specification: https://atproto.com/specs/at-uri-scheme </remarks>
val value: AtUri -> string
<summary> Extract the string representation of an AT-URI. </summary>
<param name="atUri">The AT-URI to extract the value from.</param>
<returns>The full AT-URI string (e.g. <c>"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3k2la3b"</c>).</returns>
<summary> Extract the string representation of an AT-URI. </summary>
<param name="atUri">The AT-URI to extract the value from.</param>
<returns>The full AT-URI string (e.g. <c>"at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3k2la3b"</c>).</returns>
LikeRef.Uri: AtUri
<summary>The AT-URI of the like record.</summary>
<summary>The AT-URI of the like record.</summary>
FollowRef.Uri: AtUri
<summary>The AT-URI of the follow record.</summary>
<summary>The AT-URI of the follow record.</summary>