Header menu logo FSharp.ATProto

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.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>
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
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>
val test: name: string -> TestCaseBuilder
<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 ...
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>
val equal: actual: 'a -> expected: 'a -> message: string -> unit (requires equality)
<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>
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>
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://&lt;authority&gt;[/&lt;collection&gt;[/&lt;rkey&gt;]]</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>
LikeRef.Uri: AtUri
<summary>The AT-URI of the like record.</summary>
FollowRef.Uri: AtUri
<summary>The AT-URI of the follow record.</summary>

Type something to start searching.